인라인 어셈블리 언어가 네이티브 C ++ 코드보다 느립니까?

인라인 어셈블리 언어와 C ++ 코드의 성능을 비교하려고했기 때문에 크기가 2000 인 두 배열을 100000 회 추가하는 함수를 작성했습니다. 코드는 다음과 같습니다.

#define TIMES 100000
void calcuC(int *x,int *y,int length)
{
    for(int i = 0; i < TIMES; i++)
    {
        for(int j = 0; j < length; j++)
            x[j] += y[j];
    }
}


void calcuAsm(int *x,int *y,int lengthOfArray)
{
    __asm
    {
        mov edi,TIMES
        start:
        mov esi,0
        mov ecx,lengthOfArray
        label:
        mov edx,x
        push edx
        mov eax,DWORD PTR [edx + esi*4]
        mov edx,y
        mov ebx,DWORD PTR [edx + esi*4]
        add eax,ebx
        pop edx
        mov [edx + esi*4],eax
        inc esi
        loop label
        dec edi
        cmp edi,0
        jnz start
    };
}

여기 있습니다 main():

int main() {
    bool errorOccured = false;
    setbuf(stdout,NULL);
    int *xC,*xAsm,*yC,*yAsm;
    xC = new int[2000];
    xAsm = new int[2000];
    yC = new int[2000];
    yAsm = new int[2000];
    for(int i = 0; i < 2000; i++)
    {
        xC[i] = 0;
        xAsm[i] = 0;
        yC[i] = i;
        yAsm[i] = i;
    }
    time_t start = clock();
    calcuC(xC,yC,2000);

    //    calcuAsm(xAsm,yAsm,2000);
    //    for(int i = 0; i < 2000; i++)
    //    {
    //        if(xC[i] != xAsm[i])
    //        {
    //            cout<<"xC["<<i<<"]="<<xC[i]<<" "<<"xAsm["<<i<<"]="<<xAsm[i]<<endl;
    //            errorOccured = true;
    //            break;
    //        }
    //    }
    //    if(errorOccured)
    //        cout<<"Error occurs!"<<endl;
    //    else
    //        cout<<"Works fine!"<<endl;

    time_t end = clock();

    //    cout<<"time = "<<(float)(end - start) / CLOCKS_PER_SEC<<"\n";

    cout<<"time = "<<end - start<<endl;
    return 0;
}

그런 다음 프로그램을 다섯 번 실행하여 프로세서 사이클을 얻습니다. 이는 시간으로 볼 수 있습니다. 위에서 언급 한 함수 중 하나만 호출 할 때마다.

그리고 여기에 결과가옵니다.

어셈블리 버전의 기능 :

Debug   Release
---------------
732        668
733        680
659        672
667        675
684        694
Average:   677

C ++ 버전의 기능 :

Debug     Release
-----------------
1068      168
 999      166
1072      231
1002      166
1114      183
Average:  182

릴리스 모드의 C ++ 코드는 어셈블리 코드보다 거의 3.7 배 빠릅니다. 왜?

필자가 작성한 어셈블리 코드가 GCC에서 생성 한 어셈블리 코드만큼 효과적이지 않은 것 같습니다. 저와 같은 일반적인 프로그래머는 컴파일러가 생성 한 상대보다 코드를 더 빨리 작성하기가 어렵습니다. 즉, 필자가 작성한 어셈블리 언어의 성능을 믿지 말고 C ++에 집중하고 어셈블리 언어를 잊어 버리지 않아야합니까?



답변

예, 대부분입니다.

우선 낮은 수준의 언어 (이 경우 어셈블리)가 항상 높은 수준의 언어 (이 경우 C ++ 및 C)보다 빠른 코드를 생성한다는 잘못된 가정에서 시작합니다. 그것은 사실이 아닙니다. C 코드는 항상 Java 코드보다 빠릅니까? 프로그래머라는 또 다른 변수가 있기 때문에 아니요. 아키텍처 세부 사항에 대한 코드와 지식을 작성하는 방식은 성능에 큰 영향을 미칩니다 (이 경우 참조).

당신은 할 수 항상 손으로 만든 어셈블리 코드가 잘 컴파일 된 코드보다 예를 생산하지만, 일반적으로 는 가상의 예 또는 단일 루틴이 아닌의 진정한 C ++ 코드의 500.000+ 라인의 프로그램). 나는 컴파일러는 더 나은 어셈블리 코드 95 %의 시간을 생산하고 생각 때로는, 일부 희귀 한 번, 당신이 조립 몇 짧은에 대한 코드를 작성해야 할 수도 있습니다 매우 사용 , 성능 중요한 루틴 또는 액세스해야 할 때 당신의 마음에 드는 높은 수준의 언어 기능 노출하지 않습니다. 이 복잡한 작업을 원하십니까? 이 멋진 답변을 여기에서 읽으십시오 .

왜 이런가요?

우선 컴파일러는 상상조차 할 수없는 최적화를 수행 할 수 있기 때문에 ( 이 짧은 목록 참조 ) 몇 초 만에 수행 할 것입니다 ( 일이 필요할 때 ).

어셈블리를 코딩 할 때는 잘 정의 된 호출 인터페이스를 사용하여 잘 정의 된 기능을 만들어야합니다. 그러나 레지스터 할당 , 상수 전파 , 공통 하위 식 제거 , 명령어 스케줄링 및 기타 복잡한 ( 예 : Polytope 모델 ) 과 같은 전체 프로그램 최적화절차 간 최적화 를 고려할 수 있습니다 . 에 RISC 아키텍처들 (예를 들어, 매우 어려운 명령 스케줄링이 몇 년 전 걱정 중지 손으로 조정 )과 현대 CISC의 CPU는 매우 긴이 파이프 라인을 너무.

일부 복잡한 마이크로 컨트롤러의 경우 컴파일러가 더 나은 (그리고 유지하기 쉬운) 최종 코드를 생성하기 때문에 시스템 라이브러리 조차 어셈블리 대신 C로 작성됩니다.

컴파일러는 때때로 자체적으로 일부 MMX / SIMDx 명령어자동으로 사용할 수 있으며 ,이를 사용하지 않으면 단순히 비교할 수 없습니다 (다른 답변은 이미 어셈블리 코드를 잘 검토했습니다). 루프의 경우 이것은 컴파일러에서 일반적으로 확인 하는 루프 최적화짧은 목록입니다 (C # 프로그램에 대한 일정이 결정되었을 때 혼자서 할 수 있다고 생각하십니까?) 어셈블리에 무언가를 쓰는 경우, 최소한 간단한 최적화 를 고려해야한다고 생각합니다 . 배열에 대한 교과서 예제 는 사이클언 롤링하는 것입니다 (컴파일 타임에 크기가 알려짐). 그것을하고 테스트를 다시 실행하십시오.

요즘에는 다른 이유로 CPU많은 다른 어셈블리 언어를 사용해야하는 경우가 드물다 . 당신은 그들 모두를 지원 하시겠습니까? 각각에는 특정 마이크로 아키텍처특정 명령어 세트가 있습니다. 그것들은 서로 다른 수의 기능 유닛을 가지고 있으며 그것들을 모두 바쁘게 유지하기 위해 조립 지침을 마련해야합니다 . C로 작성하면 PGO를 사용할 수 있지만 조립에서는 해당 특정 아키텍처에 대한 지식이 필요합니다 ( 다른 아키텍처에 대한 모든 내용을 재고하고 다시 실행 ). 소규모 작업의 경우 일반적으로 컴파일러 가 더 잘 수행하며 복잡한 작업의 경우 일반적 으로 작업이 상환되지 않습니다 (그리고컴파일러 어쨌든 더 잘 할 수 있습니다 ).

앉아서 코드를 살펴보면 어셈블리로 변환하는 것보다 알고리즘을 다시 디자인하는 데 더 많은 것을 얻을 수 있음을 알 수 있습니다 (이 위대한 게시물을 여기에서 읽으십시오 ), 고급 최적화가 있습니다 (및 컴파일러에 대한 힌트) 어셈블리 언어에 의존하기 전에 효과적으로 적용 할 수 있습니다. 종종 내장 함수를 사용하면 원하는 성능을 얻을 수 있으며 컴파일러는 여전히 대부분의 최적화를 수행 할 수 있습니다.

이 모든 것에서 5 ~ 10 배 빠른 어셈블리 코드를 생성 할 수 있더라도 고객에게 일주일의 시간지불 것인지 50 달러 빠른 CPU구매할 것인지 물어보아야합니다 . 대부분의 경우, 특히 LOB 응용 프로그램에서보다 더 극단적 인 최적화가 필요하지 않습니다.


답변

어셈블리 코드가 차선책이며 개선 될 수 있습니다.

  • 내부 루프에서 레지스터 ( EDX )를 밀고 터 뜨리고 있습니다. 루프 밖으로 이동해야합니다.
  • 루프를 반복 할 때마다 배열 포인터를 다시로드합니다. 루프 밖으로 이동해야합니다.
  • 당신은 loop지시를 사용합니다. 가장 현대적인 CPU에서 죽은 느린 것으로 알려져 (고대 조립 책 *을 사용 가능하게 결과를)
  • 수동 루프 언 롤링의 이점은 없습니다.
  • 사용 가능한 SIMD 명령어를 사용하지 않습니다 .

따라서 어셈블러와 관련된 기술을 크게 향상시키지 않으면 성능을 위해 어셈블러 코드를 작성하는 것이 적합하지 않습니다.

* 물론 loop고대 어셈블리 북에서 실제로 교훈을 받았는지 모르겠습니다 . 그러나 실제 컴파일러에서는 거의 볼 수 없습니다. 모든 컴파일러는 방출 할만 큼 똑똑하지 않기 loop때문에 IMHO 나쁘고 오래된 책에서만 볼 수 있습니다.


답변

어셈블리를 탐구하기 전에도 더 높은 수준의 코드 변환이 있습니다.

static int const TIMES = 100000;

void calcuC(int *x, int *y, int length) {
  for (int i = 0; i < TIMES; i++) {
    for (int j = 0; j < length; j++) {
      x[j] += y[j];
    }
  }
}

루프 회전 을 통해 변환 할 수 있습니다 .

static int const TIMES = 100000;

void calcuC(int *x, int *y, int length) {
    for (int j = 0; j < length; ++j) {
      for (int i = 0; i < TIMES; ++i) {
        x[j] += y[j];
      }
    }
}

메모리 로컬 리티가있는 한 훨씬 좋습니다.

이것은 더 최적화 될 수 있습니다 a += b.X 시간을하는 a += X * b것은 우리가 얻는 것과 같습니다 .

static int const TIMES = 100000;

void calcuC(int *x, int *y, int length) {
    for (int j = 0; j < length; ++j) {
      x[j] += TIMES * y[j];
    }
}

그러나 내가 좋아하는 최적화 프로그램 (LLVM) 이이 변환을 수행하지 않는 것 같습니다.

[편집] 그리고 restrict한정자를 xand로 사용 하면 변환이 수행된다는 것을 알았습니다 y. 실제로 이러한 제한없이, x[j]그리고 y[j]수 변환이 오류하게 동일한 위치 별명. [편집 종료]

어쨌든 이것은 최적화 된 C 버전이라고 생각합니다. 이미 훨씬 간단합니다. 이를 바탕으로 ASM에서의 균열은 다음과 같습니다 (Clang이 생성하도록하고, 쓸모가 없습니다).

calcuAsm:                               # @calcuAsm
.Ltmp0:
    .cfi_startproc
# BB#0:
    testl   %edx, %edx
    jle .LBB0_2
    .align  16, 0x90
.LBB0_1:                                # %.lr.ph
                                        # =>This Inner Loop Header: Depth=1
    imull   $100000, (%rsi), %eax   # imm = 0x186A0
    addl    %eax, (%rdi)
    addq    $4, %rsi
    addq    $4, %rdi
    decl    %edx
    jne .LBB0_1
.LBB0_2:                                # %._crit_edge
    ret
.Ltmp1:
    .size   calcuAsm, .Ltmp1-calcuAsm
.Ltmp2:
    .cfi_endproc

나는 그 모든 지침이 어디에서 왔는지 이해하지 못하지만, 항상 재미 있고 비교할 수있는 방법을 볼 수는 있지만 코드에서 어셈블리 버전보다는 최적화 된 C 버전을 계속 사용합니다. 훨씬 더 휴대용.


답변

짧은 대답 : 예.

긴 대답 : 예, 실제로 무엇을하고 있는지 알지 못하면 그렇게 할 이유가 없습니다.


답변

내 asm 코드를 수정했습니다.

  __asm
{
    mov ebx,TIMES
 start:
    mov ecx,lengthOfArray
    mov esi,x
    shr ecx,1
    mov edi,y
label:
    movq mm0,QWORD PTR[esi]
    paddd mm0,QWORD PTR[edi]
    add edi,8
    movq QWORD PTR[esi],mm0
    add esi,8
    dec ecx
    jnz label
    dec ebx
    jnz start
};

출시 버전의 결과 :

 Function of assembly version: 81
 Function of C++ version: 161

릴리스 모드의 어셈블리 코드는 C ++보다 거의 2 배 빠릅니다.


답변

내 손으로 쓴 어셈블리 언어의 성능을 신뢰해서는 안된다는 의미입니까?

예, 그것이 정확히 의미하는 바이며 모든 사람 에게 해당 됩니다 언어에 해당됩니다. 언어 X로 효율적인 코드를 작성하는 방법을 모르는 경우 X로 효율적인 코드를 작성하는 능력을 신뢰해서는 안됩니다. 따라서 효율적인 코드를 원한다면 다른 언어를 사용해야합니다.

어셈블리는 특히 이것에 민감합니다. 왜냐하면 당신이 보는 것이 당신이 얻는 것이기 때문입니다. CPU가 실행할 특정 명령어를 작성합니다. 고급 언어를 사용하면 코드를 변환하고 많은 비 효율성을 제거 할 수있는 컴파일러가 betweeen에 있습니다. 조립을 통해 당신은 스스로 할 수 있습니다.


답변

오늘날 어셈블리 언어를 사용하는 유일한 이유는 언어로 액세스 할 수없는 일부 기능을 사용하기위한 것입니다.

이것은 다음에 적용됩니다.

  • MMU와 같은 특정 하드웨어 기능에 액세스해야하는 커널 프로그래밍
  • 컴파일러에서 지원하지 않는 매우 특정한 벡터 또는 멀티미디어 명령어를 사용하는 고성능 프로그래밍.

그러나 현재 컴파일러는 매우 영리 d = a / b; r = a % b;합니다 .C에 그러한 연산자가없는 경우에도 분할을 계산하고 사용 가능한 경우 한 번에 나머지를 계산하는 단일 명령으로 두 개의 별도 명령문을 바꿀 수도 있습니다
.