Cache 와 Self Modifying Code Problem

원본 문서 : http://blogs.arm.com/software-enablement/141-caches-and-self-modifying-code/

이상적으로, 캐시는 프로세서와 메모리 사이에 끼여서 속도를 더 빠르게 만들어주는 마법을 부리는 로직이다.

성능에 민감한 코드를 작성할 때에는 캐시의 특성과 기능을 고려한다면 좀 더 좋다.

일반적으로 그렇게 마음속에 일반적인 캐시의 행동을 고려하면서 코드를 작정하는 것은 바람직하다.

하지만 여기에 당신이 원하는 결과를 얻기 위해서 캐시의 행동을 반드시 고려해야만 하는 몇가지 케이스를 보여주겠다.

자체 수정 코드(self modifying code)는 가장 좋은 예제 이다.

ARM 아키텍쳐는 데이터와 인스트럭션이 분리되서 액세스하는 캐시를 가지고 있다.

그것을 각각 D-Cache, I-Cache 라고 부른다. 이러한 이유로 수정된 하버드 아키텍처라고 불린다.

이런 설계에는 몇가지 장점이 있다.

CPU가 두 개의 인터페이스를 가짐으로 써 동시에 명령어와 데이터를 로드할 수 있다고 예전에 이야기 한적이 있다

이런 하버드 스타일의 아키텍처는 메모리 인터페이스을 사용하는 성능은 유용하지만,

하버드 아키텍쳐이기 때문에 발생하는 문제점이 있다.

순수한 하버드 아키텍쳐의 일반적인 결점은 명령어 메모리와 동일한 주소에 있는 데이터 메모리로 직접 접근할 수 가 없다는 점이다.

(역주) 순수한 하버드 아키텍처는 Intruction Memory 와 Data Memory가 완벽하게 분리되어야 한다.

따라서 Intruntion 주소와 Data 주소가 따로 존재하며, 서로 접근이 불가능하다.

이런 제약 조건은 ARM아키텍처에는 해당하지 않는다.

ARM에서, 당신은 명령어를 메모리에 새롭게 작성할 수 있다.

하지만 그렇게 되면 D-Cache와 I-Cache는 일관성이 없어진다.

새롭게 작성된 명령어는 I-Cache에 존재하는 기존 코드에게 가려져있게 된다.

이것은 프로세서가 예전 명령어를 수행하게 만들것이다.

문제점

가상으로 자체 수정 코드에 대해 살펴보자.

몇몇 JIT 컴파일된 코드는 런타임시에 함수 주소를 레지스터로 로드한다음 그곳으로 점프 한다.

JIT 컴파일러는 특정 함수를 새로운 주소로 점프하게 하려고 하고, 원래 점프하려고 했던 포인터 주소값을 수정하게 된다.

이렇게 런타임시에 목적 함수가 다른 번지를 호출하게 하는 작업은 JIT 컴파일러에겐 일반적인 작업이다.

왜냐하면 컴파일시에 목적지 번지를 알 수 없는 경우가 있기 때문이다.

상당히 단순화 시킨 그림으로 프로세서가 새로운 코드를 작성하기 전에 상태를 보자면 아래와 같다.

myWPEdit Image

이 경우에 I-Cache는 기존 코드를 이미 로드 했다.

만약 코드가 아직 실행되지 않았다면, 해당 코드가 I-Cache에 없을것 같지만, 아직 실행되지 않은 코드들도 있을 수 있다.

우리는 I-Cache가 기존 코드를 담고 있다고 가정한다.

프로세서는 오직 I-Cache에 들어있는 코드만을 실행시킬수 있으며, D-Cache에 있는 데이터만을 읽을 수 있다.

일반적으로 Cache를 뛰어 넘어서 메모리에 직접적으로 접근하는 것을 불가능하다.

D-cache에 있는 명령어를 바로 실행할 수 없고, I-Cache에 있는 데이터를 수정가능하게 읽거나 쓸 수 없다는 것은 중요한 의미가 있다.

이 말은 우리가 작성한 새로운 코드를 I-Cache(또는 주 메모리)에 직접적으로 써 넣을 수 없다는 말이다.

myWPEdit Image

만약 당신이 작성한 코드를 그냥 실행하려고 시도한다면, 프로세서는 단순하게 과거 코드를 수행하려고 할 것이다.

왜냐하면 I-Cache 안에는 여전히 과거 코드가 들어있고, 그게 새로운 코드와 차이가 있다는 것을 모르기 때문이다.

JIT 컴파일러 같은 자체 수정 코드를 활용하는 응용 프로그램들에게는 꽤나 귀찮은 일이다.

해결책

우리는 D-cache의 새로운 코드를 I-Cache에 넣어줄 필요가 있다.

데이터는 반드시 주 메모리로 옮겨진 다음 다시 I-Cache로 읽어들여야만 한다.

미래에 어느 시점에, 프로세서는 D-cache에 있는 새로운 코드를 메모리로 옮기게 될 것이다.

그리고 그 코드는 주 메모리에서 I-Cache로 다시 읽어들일 필요가 있다.

하지만 보통의 경우 이런 작업들은 일반적인 작업이 아니므로, 우리는 강제적으로 수행하게 만들어야 한다.

프로세서 아키텍처마다 캐시는 다를 수 있다. 따라서 여기서는 중요한 사항만 다루도록 하겠다.

현재 우리는, D-Cache에 새로운 코드를 넣었고, 이것은 주 메모리의 내용과 동일하지 않다.

이것을 Dirty data라고 한다. 이것을 주 메모리로 내보내기 위해서 Cache Clean을 해준다.

그리고 작업이 완료되기를 기다린다.

작업이 끝나면 아래와 같은 상태가 된다.

myWPEdit Image

이것을 하기 위해서 우리는 I-Cache를 invalidate 시켜주어야 한다. 그렇게 되면 아래와 같이 된다.

myWPEdit Image

만약 주 메모리에 있는 내용의 코드를 수행하려고 한다면, intruntion fetch는 I-Cache miss가 발생할 것이고,

프로세서는 주 메모리에서 해당 내용을 얻으려고 할것이다.

결과적으로 새로 수정한 코드가 우리의 의도대로 수행되게 된다.

하지만 이게 이야기의 전부는 아니다.

이것 말고도 고려해야할 몇가지 사항이 더 존재한다.

만약 프로세서에 분기 예측기(branch prediction)가 존재한다면, branch target buffer를 clear 해줄 필요가 있다.

일반적으로, 프로세서는 write buffer안에다 주 메모리로 쓰려고 하는 정보를 큐잉하고 있는다.

따라서 우리는 D-Cache를 clean할때 write buffer를 drain 해주어야 한다.

물론, 실제론 이러한 작업들은 당신이 사용하는 프로세서에 매우 specific한 것을 이기 때문에,

당신은 라이브러리 함수들을 이용함으로서 이런 작업을 할 수 있다.

당신이 그저 자제 수정 코드를 작성하고 싶은거라면, 각각의 특정한 프로세서들의 가타 부타한 세세한 내용까지 이해할 필요는 없다.

하지만 라이브러리의 함수들이 어떻게 동작하고 왜 그것이 필요한지 알고 있는 것은 중요하다.

끝으로, 우리는 pli 명령어를 이용해서 프로세서에게 새로운 코드를 I-Cache로 preload할 필요가 있다고 힌트를 주는것을 고려해 볼수 있다.

이 인스트럭션은 실제로 우리가 그 코드를 수행하려고 할때, 메모리로부터 읽어오기위해 stall되지 않게 해주므로,

괜찬은 성능 증가를 가져다 준다.

물론, 힌트를 준다고 해도 전혀 효과가 없을 수도 있다. 하지만 몇몇 구현에서는 이득을 줄 수 있다.

코드

위 내용을 이해했다고 해도, 실제 코드에서는 어떻게 관련이 있는걸까?

늘 그렇듯이 CP15와 관련된 명령어들은 non-privileged 모드에서는 수행이 불가능 하다 (대부분의 응용프로그램이 도는 모드)

운영체제들이 당신을 대신해서 해당 오퍼레이션을 수행해 준다는 의미이다.

다행스럽게도, 대부분의 시스템에서 non-previleged모드에서 캐시를 clean 또는 invalidate 할 수 있는 메커니즘을 제공하고 있다.

Linux (GCC)

In GCC on Linux, you should use the _clearcache function:

CODE

void _clearcache(char beg, char end);

Of course, there is little documentation for this important function, and you have to root around a fair bit to find out what it actually does. Essentially, _clearcache does the following (using a system call):

Clean the specified data cache range. Invalidate the specified instruction cache range. 

The start address (char beg) is inclusive, whilst the end address (char end) is exclusive.

The function will also flush the write buffer and perform any other necessary processor-specific fiddling about that you, as the caller, do not want to worry about. If you really want to know exactly what it does, you will need to look in the Linux kernel, in arch/arm/mm/cache-v7.S (or the equivalent file for whichever architecture you are using).

For an example that you can play with, here is one I made earlier.

Others

Operating System — Relevant Library Function

Linux (GCC) — _clearcache

Google Android — cacheflush

Windows CE — FlushInstructionCache

Advertisements

답글 남기기

아래 항목을 채우거나 오른쪽 아이콘 중 하나를 클릭하여 로그 인 하세요:

WordPress.com 로고

WordPress.com의 계정을 사용하여 댓글을 남깁니다. 로그아웃 /  변경 )

Google+ photo

Google+의 계정을 사용하여 댓글을 남깁니다. 로그아웃 /  변경 )

Twitter 사진

Twitter의 계정을 사용하여 댓글을 남깁니다. 로그아웃 /  변경 )

Facebook 사진

Facebook의 계정을 사용하여 댓글을 남깁니다. 로그아웃 /  변경 )

w

%s에 연결하는 중