2009-07-16 3 views
10

인라인 어셈블리를 사용하여 컴파일러를 때리는 데 어려움을 겪고 있습니다.인라인 어셈블리에서보다 빨리 구현되는 간단한 C 함수의 예는 무엇입니까?

컴파일러가 실제로 정말 빠르고 간단하게 만드는 데 어려움을 겪고있는 좋은 기능과 비예책의 예제는 무엇입니까? 인라인 어셈블리를 사용하면 상대적으로 간단합니다.

+7

당신을 선택하지는 않겠지 만 최적화를 요청하고 속도를 묻는 질문에 아주 많은 사람들이 있습니다. 요구 사항을 충족하지 못하기 때문에 필요하다고 말하는 고객은 거의 없습니다. 분명히 우리는 "조기 최적화가 모든 악마의 뿌리"라고 생각하지 않았습니다. –

+0

내 질문에 대한 답은 제가 iPhone에서 인라인 어셈블리를 사용하여 주위를 핥고 있었고 블로그 게시물을 작성하려고했습니다. . 하지만 내 인생은 내 컴파일러를 능가 할 수 없었습니다. 그래서 나는 컴파일러가 비효율적 인 코드를 생성하는 알려진 경우가 있는지 궁금해합니다. –

+1

ARM 어셈블리는 "클리너"명령어 세트 중 하나입니다. RISC 프로세서의 철학 중 일부는 컴파일러에서 쉽게 사용하지 않는 지침을 추가하지 않는 것입니다. 특정 ARM 변형의 명령어 세트를보고 명확한 C 변환이없는 opcode를 찾아야합니다. – NoMoreZealots

답변

7

아이폰과 어셈블리 코드와 관련이 있기 때문에 아이폰 세계에서 관련이있을만한 예제를 제공 할 것입니다 (일부 sse 또는 x86 asm이 아님). 누군가 실제 응용 프로그램을위한 어셈블리 코드를 작성하기로 결정하면 디지털 신호 처리 또는 이미지 조작이 될 가능성이 높습니다. 예 : RGB 픽셀의 색상 공간 변환, 이미지 인코딩 jpeg/png 형식 또는 사운드를 mp3, amr 또는 g729로 인코딩하여 VoIP 응용 프로그램 용. 사운드 인코딩의 경우에는 컴파일러에서 효율적인 asm 코드로 변환 할 수없는 많은 루틴이 있습니다. C에서 이와 동등한 기능이 없습니다. 사운드 처리에서 일반적으로 사용되는 예 : 포화 된 수학, 곱하기 누적 루틴, 행렬 곱셈 .

포화 된 추가의 예 : 32 비트 부호있는 int의 범위는 0x8000 0000 < = int32 < = 0x7fff ffff입니다. 두 개의 int를 추가하면 결과가 오버플로 될 수 있지만 디지털 신호 처리의 특정 경우에는 받아 들일 수 없습니다. 기본적으로 결과 오버플로 또는 언더 플로우가 포화 된 경우 0x8000 0000 또는 0x7fff ffff를 반환해야합니다. 그것을 확인하는 완전한 함수가 될 것입니다.포화 추가의 최적화 된 버전이 될 수있다 :

 
int saturated_add(int a, int b) 
{ 
    int result = a + b; 

    if (((a^b) & 0x80000000) == 0) 
    { 
     if ((result^a) & 0x80000000) 
     { 
      result = (a < 0) ? 0x80000000 : 0x7fffffff; 
     } 
    } 
    return result; 
} 

당신은 또한/다른 오버 플로우 또는 당신이 (또한 ASM을 사용하는 데 필요한) 오버 플로우 플래그를 체크 할 수있다 x86에서 여러 확인 할 수 있습니다합니다. iPhone은 dsp asm을 사용하는 armv6 또는 v7 cpu를 사용합니다. 따라서 복수 브런치 (if/else 문) 및 2 32 비트 상수를 사용하는 saturated_add 함수는 하나의 CPU 사이클을 사용하는 하나의 간단한 asm 명령이 될 수 있습니다. 따라서 단순히 asm 명령을 사용하기 위해 saturated_add를 만드는 것은 전체 알고리즘을 2-3 배 빠르게 (크기는 더 작게) 만들 수 있습니다. 여기에 QADD 설명서의 : 종종 긴 루프에서 실행되는 코드의 QADD

다른 예이다

 
res1 = a + b1*c1; 
res2 = a + b2*c2; 
res3 = a + b3*c3; 

아무것도 여기에 최적화 될 수없는 것 같다,하지만 ARM의 CPU에 특정 DSP 명령어를 사용할 수 간단한 곱셈을하는 것보다 적은 사이클을 잡으십시오! 맞습니다. 구체적인 지침이있는 a + b * c는 단순한 a * b보다 빠르게 실행될 수 있습니다. 이런 종류의 경우 컴파일러는 코드 논리를 이해할 수 없으므로 이러한 dsp 명령어를 직접 사용할 수 없으므로 코드를 최적화하기 위해 수동으로 asm을 작성해야하지만 코드의 일부만 수동으로 작성해야합니다. 최적화. 수동으로 간단한 루프를 작성하기 시작하면 거의 확실하게 컴파일러를 이길 수 없습니다! 웹상에 전나무 필터, amr 인코딩/디코딩 등을 코드하는 인라인 어셈블러에 대한 여러 가지 좋은 논문이 있습니다.

0

컴파일러를 통해 얻은 가장 좋은 결과는 간단한 memcpy 루틴이었습니다 ... 기본 설정 항목을 많이 건너 뛰었습니다 (예 : 스택 프레임을 많이 필요로하지 않으므로 몇 사이클을 절약 할 수 있습니다.), 그리고 꽤 털이 많은 것을했다.

약 6 년 전, 알려지지 않은 품질의 독점 컴파일러로 이루어졌습니다. 내가 가진 코드를 파헤쳐 GCC와 비교해보아야 할 것이다. 나는 그것이 더 빨라질 수 있다는 것을 모른다. 그러나 나는 그것을 배제하지 않을 것이다.

필자의 memcpy가 C 라이브러리의 평균보다 15 배 더 빠르더라도 필자는 필자가 필요로 할 때를 대비하여 나의 백 포켓에 보관했다. PPC 어셈블리를 가지고 노는 것은 나를위한 장난감이었고, 우리의 어플리케이션에는 속도 향상이 필요하지 않았습니다.

2

SIMD 작업과 같은 작업을 수행하려는 경우 컴파일러를 능가 할 수 있습니다. 이것은 아키텍처와 명령어 세트에 대한 지식이 필요합니다.

+0

어셈블리를 처리 할 때 아키텍처 및 명령어 세트를 이해하는 중요성을 과소 평가할 수는 없습니다. 저는 일반적으로 asm을 피합니다. 그러나 이론적 인 성능에 대한 아이디어를 가질 수 있도록 아키텍처의 기능을 배우는 것이 중요합니다. – NoMoreZealots

8

당신이 SIMD 작업 (그것도 자동 벡터화 (autovectorization)이있는 경우!), 당신은 일반적으로 컴파일러의 자동 벡터화 (autovectorization) 능력보다 훨씬 더 수행 SIMD 어셈블리를 쓸 수있는 86 년대의

Here's 아주 기본적인 SSE (한 바람을 피우고 고려하지 않는 경우 SIMD 명령어 세트) 자습서. Visual C++ 인라인 어셈블리 용입니다.

편집 : 다음은 자신을 위해 시도하려는 작은 쌍의 기능입니다. 그것은 n 길이 내적을 계산하는 것입니다. 하나는 SSE 2 명령 인라인 (GCC 인라인 구문)을 사용하고 다른 하나는 매우 기본입니다.

아주 간단합니다. 좋은 컴파일러가 단순한 C 루프를 벡터화 할 수 없다면 매우 놀랍습니다. , 그렇지 않으면 SSE2의 속도가 빨라야합니다. SSE 2 버전은 레지스터를 더 많이 사용하면 더 빠를 수 있지만 매우 약한 SSE 기술을 늘리고 싶지는 않습니다. :)

float dot_asm(float *a, float*b, int n) 
{ 
    float ans = 0; 
    int i; 
    // I'm not doing checking for size % 8 != 0 arrays. 
    while(n > 0) { 
    float tmp[4] __attribute__ ((aligned(16))); 

    __asm__ __volatile__(
      "xorps  %%xmm0, %%xmm0\n\t" 
      "movups  (%0), %%xmm1\n\t" 
      "movups  16(%0), %%xmm2\n\t" 
      "movups  (%1), %%xmm3\n\t" 
      "movups  16(%1), %%xmm4\n\t" 
      "add  $32,%0\n\t" 
      "add  $32,%1\n\t" 
      "mulps  %%xmm3, %%xmm1\n\t" 
      "mulps  %%xmm4, %%xmm2\n\t" 
      "addps  %%xmm2, %%xmm1\n\t" 
      "addps  %%xmm1, %%xmm0" 
      :"+r" (a), "+r" (b) 
      : 
      :"xmm0", "xmm1", "xmm2", "xmm3", "xmm4"); 

    __asm__ __volatile__(
     "movaps  %%xmm0, %0" 
     : "=m" (tmp) 
     : 
     :"xmm0", "memory");    

    for(i = 0; i < 4; i++) { 
     ans += tmp[i]; 
    } 
    n -= 8; 
    } 
    return ans; 
} 

float dot_c(float *a, float *b, int n) { 

    float ans = 0; 
    int i; 
    for(i = 0;i < n; i++) { 
    ans += a[i]*b[i]; 
    } 
    return ans; 
} 
+1

SIMD는 분명히 부정 행위를하지 않습니다. 컴파일러가 하드웨어를 따라 잡지 못하는 경우에 대한 명확한 사례를 제공합니다. C는 명령 수준 병렬 처리를 잘 처리하지 못합니다. 어쩌면 여기 저기에 풀려나는 고리가있을 수 있지만 더 진보 된 일과에는 심각한 조정이 필요합니다. – NoMoreZealots

+0

SIMD 명령어를 출력 할 컴파일러가 많이 있습니다. – jrockway

+0

제한된 경우에 해당됩니다. 기본적으로 코드가 일반적인 기법이나 알고리즘으로 작성되는 한. 일단 명령어 세트가 너무 커지면 컴파일러 나 최적화 프로그램을 단순히 복잡성 때문에 쓰기 만하면 많은 명령어를 최적으로 사용할 수 있습니다. 이는 "RISC"프로세서 개념의 기초가 된 부분입니다. 최적화는 체스와 비슷하지만 컴퓨터가 대다수 사람들을 꺾을 수는 있지만 데스크톱보다 그랜드 마스터를이기는 데는 많은 시간이 걸립니다. – NoMoreZealots

6

당신이 assembly guru을하지 않는 한 컴파일러를 치는 확률이 매우 낮은 있습니다.

상기 링크에서 단편

예를 들어, "% EAX XOR %의 EAX"명령어는 초기 세대 제로 에 레지스터를 설정 빠른 방법 비트 중심이었다 x86의 경우 이지만 대부분의 코드는 컴파일러 및 컴파일러에서 생성됩니다. 은 XOR 명령어를 생성하지 않습니다. 그래서 IA 디자이너, 최대 문자 그대로 "MOVL $ 0 % EAX" 명령을 만드는 조합 디코딩 로직 의 전면 에 자주 발생하는 컴파일러 생성 지침을 이동하기로 결정은 XOR 명령보다 더 빨리 실행됩니다.

+4

저는 어셈블리 전문가가 아니며 컴파일러를 이겼습니다. 나는 거의 어셈블리에 의지하지 않는다.내가해야 할 때 최후의 수단이었다. 이것은 그냥 말하지 않는 것처럼 보입니다. 그리고 그것은 그의 질문을 무시합니다. 그는 그 질문에서 쉽지 않다는 것을 인정한다. – NoMoreZealots

+1

나는 그것이 불가능하다고 말하지 않았다. 명령어 집합을 괴롭히는 경우보다 빠른 코드를 작성하거나 더 적은 수의 명령어로 루틴을 집어 넣으려고 할 수 있습니다. 매우 정교하지 않은 컴파일러가 있거나 컴파일러가 sse, 3dnow 세트를 처리하지 못하는 경우 어셈블리 작성이 일부 루틴을 구현하는 * 적절한 방법 일 수 있습니다. –

+1

당신이 맞다면, 당신이 complier를 때리기의 어떤 희망도 갖고 싶은 경우에 명령 세트를 이해하는 것은 절대 필요합니다. 그러나 훌륭한 컴파일러를 사용하더라도 현대 아키텍처에서 잘 매핑되는 C 구문이없는 명령어를 찾을 수 있습니다. 멀티 코어 패러다임이 표준이되면서 커지는 추상화에는 여전히 "간격"이 있습니다. 오늘날의 전력 소비 및 이동 시장에서 우리는 애플리케이션에서 CPU 코어 속도가 더 빠르다고 가정 할 수 없습니다. 1999 년에 CPU가 1GHz를 기록했고, 새로운 응용 프로그램이 "가장 뜨거웠 던"하드에서 현재 400Mhz로 클록 킹하고 있습니다. – NoMoreZealots

5

일반적인 "해협 C"구현을 사용하여 간단한 상호 상관 관계를 구현했습니다. 그런 다음 내가 사용할 수있는 타임 슬라이스보다 더 오래 걸렸을 때, 알고리즘의 명시적인 병렬 처리와 프로세서 내장 함수를 사용하여 특정 명령어를 강제로 계산에 사용하도록했습니다. 이 특별한 경우에 대해 계산 시간은 30ms 이상에서 4ms 이상으로 단축되었습니다. 다음 데이터 수집이 발생하기 전에 처리를 완료하기 위해 15ms의 창이있었습니다.

이것은 VLWI 프로세서에서의 SIMD 유형 최적화였습니다. 이 작업은 기본적으로 4 개 정도의 프로세서 내장 함수를 필요로합니다.이 내장 함수는 기본적으로 소스 코드에서 함수 호출의 모양을 나타내는 어셈블리 언어 명령어입니다. 인라인 어셈블리에서도 동일한 작업을 수행 할 수 있지만 구문 및 레지스터 관리는 프로세서 내장 함수로 약간 더 멋지다.

크기가 중요하다면 어셈블러는 왕이 아닙니다. 나는 512 바이트 이하로 전체 화면 텍스트 편집기를 작성한 남자와 학교에 다녔다.

+0

이것은 어셈블러가 합리적인 전형적인 경우입니다. 코드는 C로 작성되었습니다. 잘되었지만 충분히 빠르지는 않습니다. 어셈블러에서 레코딩을하면 작업 속도가 빨라 졌기 때문에 어셈블러에 들러야 할 좋은 이유가되었습니다. –

+0

해트트 C 버전에서 나온 성능에 나는 실망했다. 칩 벤더의 선전은 C 컴파일러가 얼마나 좋은지 자랑했다. 그리고 그들은 가장 최근의 툴체인이 그것을 최적화하는 어떤 더 나은 일도하지 않습니다. 불행히도 VLWI가있는 DSP는 최적화 프로그램을 작성하기 쉽지 않습니다. – NoMoreZealots

5

단어를 특정 비트 수만큼 회전해야하는 체크섬 알고리즘이 있습니다.

//rotate word n right by b bits 
#define ROR16(n,b) (((n)>>(b))|(((n)<<(16-(b)))&0xFFFF)) 

//... and inside the inner loop: 
sum ^= ROR16(val, pos); 

으로 VisualStudio의 릴리스 빌드이로 확장 :를 구현하려면,이 매크로를 가지고 (valsum이 BX에, pos는 DX에, 도끼에)

mov   ecx,10h 
sub   ecx,edx 
mov   ebp,eax 
shl   ebp,cl 
mov   cx,dx 
sar   ax,cl 
add   esi,2 
or   bp,ax 
xor   bx,bp 

더 많은 효율적인 동등 손으로 생성 된 어셈블리는 다음과 같습니다 제가 파악하지 않은

mov  cl,dx 
ror  ax,cl 
xor  bx,ax 

순수 'C'에서 ror 명령을 방출하는 방법 암호. 그러나 ...
이 글을 쓰면서 컴파일러 내장 함수를 기억해 냈습니다. 나는과 지침의 두 번째 세트를 생성 할 수 있습니다

sum ^= _rotr16(val,pos); 

그래서 내 대답은 : 당신은, 순수한 C 컴파일러를 이길 어셈블리를 인라인 의지하기 전에 내장 함수를 확인할 수 있다고 생각하더라도.

+0

좋은 예입니다. – NoMoreZealots

+0

gcc (4.0.1)에서 -O4와 함께 시도했습니다. 32 비트 회전의 경우 ROR 명령어를 출력하지만 16 비트의 경우는 출력하지 않습니다. – finnw