C에서 쉬프팅과 곱셈의 시간 차이를 테스트 할 때 차이가 없습니다. 왜?


28

나는 이진수를 바꾸는 것이 2 ^ k를 곱하는 것보다 훨씬 효율적이라는 것을 배웠다. 그래서 나는 실험하고 싶었고 다음 코드를 사용하여 이것을 테스트했습니다.

#include <time.h>
#include <stdio.h>

int main() {
    clock_t launch = clock();
    int test = 0x01;
    int runs;

    //simple loop that oscillates between int 1 and int 2
    for (runs = 0; runs < 100000000; runs++) {


    // I first compiled + ran it a few times with this:
    test *= 2;

    // then I recompiled + ran it a few times with:
    test <<= 1;

    // set back to 1 each time
    test >>= 1;
    }

    clock_t done = clock();
    double diff = (done - launch);
    printf("%f\n",diff);
}

두 버전 모두 출력은 약 440000이며 10000을 주거나받습니다. 두 버전의 출력 간에는 (시각적으로는) 큰 차이가 없었습니다. 내 질문은 내 방법론에 문제가 있습니까? 시각적 차이가 있어야합니까? 이것은 내 컴퓨터의 아키텍처, 컴파일러 또는 다른 것과 관련이 있습니까?


47
누구든지 당신을 분명히 가르쳤다. 1970 년대 이후 일반적으로 사용되는 아키텍처에서 일반적으로 사용되는 컴파일러의 경우 이러한 믿음은 사실이 아닙니다. 이 주장을 테스트 해 주셔서 감사합니다. 나는 하늘을 위해 자바 스크립트에 대한 이 말도 안되는 주장을 들었다 .
에릭 Lippert의

21
이와 같은 질문에 대답하는 가장 좋은 방법은 컴파일러가 생성하는 어셈블리 코드를 보는 것입니다. 컴파일러에는 일반적으로 생성중인 어셈블리 언어의 사본을 생성 할 수있는 옵션이 있습니다. GNU GCC 컴파일러의 경우 '-S'입니다.
Charles E. Grant

8
로 이것을 찾은 후에 gcc -S대한 코드 test *= 2는 실제로 루프 shll $1, %eaxgcc -O3 -S없을 때 호출 될 때 지적해야합니다. 두 시계의 호출은 떨어져 라인이다 :callq _clock movq %rax, %rbx callq _clock

6
"이진수를 바꾸는 것이 2 ^ k를 곱하는 것보다 훨씬 효율적이라는 것을 배웠다"; 우리는 틀린 것으로 밝혀 지거나 적어도 구식 인 많은 것들을 배웁니다. 똑똑한 컴파일러는 둘 다 동일한 시프트 연산을 사용합니다.
John Bode

9
이러한 종류의 최적화 작업을 수행 할 때는 항상 생성 된 어셈블리 코드를 확인하여 측정 대상으로 생각하는 것을 측정하고 있는지 확인하십시오. SO에 대한 많은 수의 "이러한 이유는 무엇입니까?"라는 질문은 결과가 사용되지 않기 때문에 연산을 완전히 제거하는 컴파일러로 끓어 올라갑니다.
Russell Borogove

답변:


44

다른 답변에서 말했듯이 대부분의 컴파일러는 비트 시프트로 수행되도록 곱셈을 자동으로 최적화합니다.

이것은 최적화 할 때 매우 일반적인 규칙입니다. 대부분의 '최적화'는 실제로 의미하는 바에 대해 컴파일을 잘못 안내하고 성능을 저하시킬 수 있습니다.

성능 문제를 발견하고 문제가 무엇인지 측정 한 경우에만 최적화하십시오. (우리가 작성한 대부분의 코드는 자주 실행되지 않으므로 귀찮게 할 필요가 없습니다)

최적화의 큰 단점은 '최적화 된'코드를 읽을 수없는 경우가 많다는 것입니다. 따라서 귀하의 경우에는 곱셈을 할 때 항상 곱셈을하십시오. 비트를 이동하고 싶을 때 비트 이동을 수행하십시오.


20
의미 적으로 올바른 작업을 항상 사용하십시오. 비트 마스크를 조작하거나 더 큰 정수 내에 작은 정수를 배치하는 경우 shift가 적절한 작업입니다.
ddyer

2
고급 소프트웨어 애플리케이션에서 시프트 연산자에 대한 곱셈을 최적화해야 할 필요가 있습니까? 컴파일러가 이미 최적화했기 때문에이 지식을 얻는 것이 유용한 유일한 시간은 매우 낮은 수준 (적어도 컴파일러 아래)에서 프로그래밍하는 것입니다.
NicholasFolk

11
트윗 담아 가기 가장 이해하기 쉬운 것을하십시오. 어셈블리를 직접 작성하는 경우 유용 할 수 있습니다 ... 또는 최적화 컴파일러를 작성하는 경우 다시 유용 할 수 있습니다. 그러나 그 두 가지 경우를 제외하고는 자신이하는 일을 모호하게하고 다음 프로그래머 ( 당신이 사는 곳을 아는 도끼 살인자)를 만드는 트릭이 당신의 이름을 저주하고 취미를 생각할 것입니다.

2
@NicholasFolk :이 수준에서의 최적화는 거의 항상 CPU 아키텍처에 의해 가려 지거나 약해집니다. 메모리에서 인수를 가져 와서 다시 쓸 때 50 사이클을 절약하면 누가 걱정합니까? 이와 같은 마이크로 최적화는 메모리가 CPU 속도에서 (또는 그와 비슷한) 실행되었을 때 의미가 있었지만 오늘날에는 그리 많지 않았습니다.
TMN

2
나는 그 인용문의 10 %를 보는 것에 지 쳤고, 여기 머리에 못을 박았 기 때문에 : "효율의 성배는 남용으로 이어질 것이라는 데는 의심의 여지가 없습니다. 프로그래머들은 생각하거나 걱정하는 데 많은 시간을 낭비합니다. 프로그램의 중요하지 않은 부분의 속도와 효율성에 대한 이러한 시도는 실제로 디버깅 및 유지 관리를 고려할 때 큰 부정적인 영향을 미칩니다. 우리 시간의 약 97 %와 같은 작은 효율성을 잊어야 합니다 . 모든 악. ...
cHao

25

컴파일러는 상수를 인식하고 적절한 경우 곱셈을 시프트로 변환합니다.


컴파일러는 2의 거듭 제곱 인 상수를 인식하고 시프트로 변환합니다. 모든 상수를 시프트로 변경할 수있는 것은 아닙니다.
quick_now

4
@quickly_now : 시프트와 덧셈 / 뺄셈의 조합으로 변환 될 수 있습니다.
Mehrdad

2
고전적인 컴파일러 최적화 버그는 나누기를 올바른 시프트로 변환하는 것입니다. 이는 긍정적 인 배당에는 효과가 있지만 네거티브에는 1 씩 꺼져 있습니다.
ddyer

1
@quickly_now 나는 '적절한 곳'이라는 용어가 일부 상수를 시프트로 다시 쓸 수 없다는 생각을 포함한다고 생각합니다.
Pharap

21

곱셈보다 시프트가 빠른지 여부는 CPU 아키텍처에 따라 다릅니다. 펜티엄과 그 이전 시절에 곱셈의 1 비트 수에 따라 이동이 곱셈보다 빠릅니다. 예를 들어, 곱하기가 320 인 경우 101000000, 2 비트입니다.

a *= 320;               // Slower
a = (a<<7) + (a<<9);    // Faster

그러나 두 비트 이상이 있다면 ...

a *= 324;                        // About same speed
a = (a<<2) + (a<<7) + (a<<9);    // About same speed

a *= 340;                                 // Faster
a = (a<<2) + (a<<4) + (a<<7) + (a<<9);    // Slower

단일 사이클을 곱하지만 배럴 시프터가 없는 PIC18 과 같은 작은 마이크로 컨트롤러에서 1 비트 이상 이동하면 곱셈이 더 빠릅니다.

a  *= 2;   // Exactly the same speed
a <<= 1;   // Exactly the same speed

a  *= 4;   // Faster
a <<= 2;   // Slower

이는 구형 인텔 CPU 에서와는 정반대 입니다.

그러나 여전히 그렇게 간단하지 않습니다. Superscalar 아키텍처로 인해 올바르게 기억한다면 Pentium은 하나의 곱하기 명령어 또는 두 개의 시프트 명령어를 동시에 처리 할 수있었습니다 (서로 의존하지 않는 한). 즉, 변수에 2의 거듭 제곱 을 곱하려면 이동이 더 나을 수 있습니다.

a  *= 4;   // 
b  *= 4;   // 

a <<= 2;   // Both lines execute in a single cycle
b <<= 2;   // 

5
+1 "이동이 곱셈보다 빠른지 여부는 CPU 아키텍처에 따라 다릅니다." 실제로 역사에 조금 들어가서 대부분의 컴퓨터 신화가 실제로 논리적 근거를 가지고 있음을 보여 주셔서 감사합니다.
Pharap

11

테스트 프로그램에 몇 가지 문제가 있습니다.

먼저 실제로의 값을 사용하지 않습니다 test. C 표준 내에서는 가치가 test중요하지 않습니다. 옵티마이 저는이를 완전히 제거 할 수 있습니다. 일단 제거되면 루프는 실제로 비어 있습니다. 눈에 띄는 효과는 설정하는 runs = 100000000것이지만runs 사용되지는 않습니다. 따라서 옵티마이 저는 전체 루프를 제거 할 수 있습니다. 손쉬운 수정 : 계산 된 값도 인쇄합니다. 충분히 결정된 최적화 프로그램은 여전히 ​​루프를 최적화 할 수 있습니다 (컴파일 타임에 알려진 상수에 전적으로 의존 함).

둘째, 서로를 취소하는 두 가지 작업을 수행합니다. 옵티마이 저는이를 인식하고 취소 할 수 있습니다. 다시 빈 루프를 남기고 제거하십시오. 이것은 수정하기가 매우 어렵습니다. unsigned int오버플로는 정의되지 않은 동작으로 전환 할 수는 있지만 물론 결과는 0입니다. 그리고 간단한 일 (예 test += 1:)은 최적화 프로그램이 알아낼 수있을 정도로 쉽습니다.

마지막으로 test *= 2실제로 곱셈으로 컴파일 될 것이라고 가정합니다 . 그것은 매우 간단한 최적화입니다. 비트 시프트가 더 빠르면 옵티마이 저가 대신 사용합니다. 이 문제를 해결하려면 구현 별 어셈블리 인라인과 같은 것을 사용해야합니다.

또는 마이크로 프로세서 데이터 시트를 확인하여 어느 것이 더 빠른지 확인하십시오.

gcc -S -O3버전 4.9 를 사용 하여 프로그램을 컴파일 한 어셈블리 출력을 확인했을 때 옵티마이 저는 실제로 위의 모든 간단한 변형과 ​​여러 가지를 실제로 보았습니다. 모든 경우에 루프를 제거하고 (상수를에 할당 test) 남은 것은에 대한 호출 clock(), 변환 / 빼기 및 printf.


1
또한 옵티마이 저는 sqrt c # vs sqrt c ++에 표시된 것처럼 상수 (루프에서도)의 연산을 최적화 할 수 있으며 옵티마이 저는 값을 합산 한 루프를 실제 합계로 대체 할 수 있습니다. 해당 최적화를 무효화하려면 런타임시 결정된 항목 (예 : 명령 행 인수)을 사용해야합니다.

@MichaelT p. "충분히 결정된 옵티마이 저는 여전히 루프를 최적화 할 수 있습니다. (컴파일 타임에 알려진 상수에 전적으로 의존합니다.")
derobert

나는 당신이 말하는 것을 얻었지만 컴파일러가 전체 루프를 제거한다고 생각하지 않습니다. 반복 횟수를 늘리면이 이론을 쉽게 테스트 할 수 있습니다. 반복을 늘리면 프로그램이 더 오래 걸립니다. 루프가 완전히 제거 된 경우에는 해당되지 않습니다.
DollarAkshay 14시 37 분

@AkshayLAradhya 내가 무슨 말을 할 수없는 당신의 컴파일러가하고있는,하지만 난 것을 다시 확인 gcc -O3(현재 7.3) 아직 완전히 루프를 제거합니다. 필요한 경우 int 대신 long으로 전환해야합니다. 그렇지 않으면 오버플로로 인해 무한 루프로 최적화됩니다.
derobert

8

나는 질문과 일부 답변 또는 의견에서 여러 가지 가정을 보았 기 때문에 질문자가 더 차별화 된 답변을 얻는 것이 더 도움이 될 것이라고 생각합니다.

시프트 및 곱셈의 상대적 런타임 결과는 C와 관련이 없습니다. C라고 말할 때, GCC의 해당 버전이나 언어와 같은 특정 구현의 인스턴스를 의미하지는 않습니다. 나는이 광고를 불합리하게 생각하는 것이 아니라 설명을 위해 극단적 인 예를 사용하는 것을 의미합니다. 완전히 표준을 준수하는 C 컴파일러를 구현하고 곱셈은 1 밀리 초가 걸리거나 다른 방법으로 시간이 걸립니다. C 또는 C ++에서 이러한 성능 제한을 인식하지 못합니다.

논증에서이 기술에 관심이 없을 수도 있습니다. 당신의 의도는 아마도 시프트 대 곱셈의 상대적인 성능을 테스트하는 것이었고 C를 선택했습니다. 일반적으로 저수준 프로그래밍 언어로 인식되므로 소스 코드가 해당 명령으로 더 직접적으로 번역 될 것으로 기대할 수 있습니다. 그러한 질문은 매우 일반적이며 C 에서조차도 소스 코드가 주어진 인스턴스에서 생각하는 것처럼 직접 명령어로 변환되지 않는다는 것이 좋은 대답이라고 지적합니다. 아래에 가능한 컴파일 결과가 있습니다.

여기에서 실제 소프트웨어에서이 동등성을 대체 할 때 유용성을 묻는 의견이 들어옵니다. Eric Lippert의 의견과 같이 귀하의 질문에 대한 의견에서 일부를 볼 수 있습니다. 이러한 최적화에 대한 응답으로 더 노련한 엔지니어로부터 일반적으로 얻을 수있는 반응과 일치합니다. 생산 코드에서 이진 이동을 곱셈 및 나누기의 담요 수단으로 사용하는 경우 사람들은 대부분 코드를 비판하고 어느 정도의 감정적 반응을 보일 것입니다. 초보 프로그래머에게는 이러한 반응의 이유를 더 잘 이해하지 않는 한 이해가되지 않을 수 있습니다.

이러한 이유는 기본적으로 상대 성능을 비교하여 알 수 있듯이 이러한 최적화의 가독성과 유용성이 감소 된 조합입니다. 그러나 나는 곱셈을위한 교대의 대체가 그러한 최적화의 유일한 예라면 사람들이 강한 반응을 보일 것이라고 생각하지 않습니다. 당신과 같은 질문은 종종 다양한 형태와 다양한 맥락으로 나타납니다. 더 많은 고위 엔지니어들이 실제로 때때로 강하게 반응하는 것은 사람들이 코드 기반에서 그러한 미세 최적화를 자유롭게 사용할 때 훨씬 더 넓은 범위의 피해가 발생할 수 있다고 생각합니다. 대규모 코드 기반으로 Microsoft와 같은 회사에서 근무하는 경우 다른 엔지니어의 소스 코드를 읽거나 특정 코드를 찾으려고 많은 시간을 할애합니다. 호출기에서 수신 된 호출에 따라 프로덕션 중단을 수정해야하는 경우와 같이, 몇 년 후, 특히 가장 부적절한 시간에 이해하려고 시도하는 코드 일 수도 있습니다. 금요일 밤의 의무, 친구들과 즐거운 밤을 보내려고… 코드를 읽는 데 많은 시간을 할애한다면 가능한 한 읽을 수있는 것이 좋습니다. 좋아하는 소설을 읽는다고 상상해보십시오. 그러나 출판사에서 abbrv를 사용하는 새 버전을 출시하기로 결정했습니다. 모든 ovr th plc bcs thy thnk it svs spc. 이는 다른 엔지니어가 코드에 이러한 최적화를 적용하면 코드에 대한 반응과 유사합니다. 다른 답변에서 지적했듯이 의미를 명확하게 진술하는 것이 좋습니다.

그러나 그러한 환경에서도이 동등성 또는 다른 동등성을 알고있을 것으로 예상되는 인터뷰 질문을 스스로 해결할 수 있습니다. 그것들을 아는 것은 나쁘지 않으며 훌륭한 엔지니어는 이진 이동의 산술 효과를 알고있을 것입니다. 나는 이것이 좋은 엔지니어가된다고 말하지는 않았지만, 좋은 엔지니어는 제 생각에 알 것입니다. 특히, 일반적으로 인터뷰 루프가 끝날 때까지 일부 관리자를 찾을 수 있습니다.이 관리자는이 스마트 엔지니어링 "트릭"을 코딩 문제로 드러내고 자신이 또한 숙련 된 엔지니어 중 하나이거나 관리자 일뿐 아니라 관리자 일뿐입니다. 그러한 상황에서는 감동적인 인상을 남기고 깨달은 인터뷰에 감사하십시오.

C에서 속도 차이가 보이지 않는 이유는 무엇입니까? 가장 가능성이 높은 답변은 동일한 어셈블리 코드를 생성한다는 것입니다.

int shift(int i) { return i << 2; }
int multiply(int i) { return i * 2; }

둘 다 컴파일 할 수 있습니다

shift(int):
    lea eax, [0+rdi*4]
    ret

최적화가없는 GCC, 즉 "-O0"플래그를 사용하면 다음과 같은 결과를 얻을 수 있습니다.

shift(int):
    push    rbp
    mov rbp, rsp
    mov DWORD PTR [rbp-4], edi
    mov eax, DWORD PTR [rbp-4]
    sal eax, 2
    pop rbp
    ret
multiply(int):
    push    rbp
    mov rbp, rsp
    mov DWORD PTR [rbp-4], edi
    mov eax, DWORD PTR [rbp-4]
    add eax, eax
    pop rbp
    ret

보다시피, "-O0"을 GCC에 전달한다고해서 그것이 어떤 종류의 코드를 생성하는지에 대해 현명하지는 않을 것입니다. 특히,이 경우에도 컴파일러는 곱하기 명령어 사용을 피했습니다. 다른 숫자만큼의 이동과 2의 거듭 제곱이 아닌 숫자의 곱셈으로 동일한 실험을 반복 할 수 있습니다. 플랫폼에서 시프트와 추가의 조합을 볼 수 있지만 곱셈은 표시되지 않을 수 있습니다. 곱셈과 시프트가 실제로 같은 비용을 가졌다면 컴파일러가 모든 경우에 곱셈을 사용하지 않는 것이 약간 우연의 일치 인 것 같습니다. 그렇지 않습니까? 그러나 증거를위한 가정을 제공하는 것은 아닙니다.

위 코드를 사용하여 테스트를 다시 실행하여 속도 차이를 확인할 수 있습니다. 그럼에도 불구하고 곱셈이 없다는 것을 알 수 있듯이 시프트 대 곱셈을 테스트하지 않지만 GCC가 시프트의 C 연산을 위해 특정 플래그 세트로 생성 한 코드는 특정 인스턴스에서 곱합니다 . 따라서 다른 테스트에서는 직접 어셈블리 코드를 편집하고 대신 "곱하기"방법의 코드에 "imul"명령어를 사용할 수 있습니다.

컴파일러의 일부 스마트를 물리 치고 싶다면보다 일반적인 시프트 및 곱셈 방법을 정의하고 다음과 같이 끝납니다.

int shift(int i, int j) { return i << j; }
int multiply(int i, int j) { return i * j; }

다음과 같은 어셈블리 코드가 생성 될 수 있습니다.

shift(int, int):
    mov eax, edi
    mov ecx, esi
    sal eax, cl
    ret
multiply(int, int):
    mov eax, edi
    imul    eax, esi
    ret

마지막으로 GCC 4.9의 최고 최적화 수준에서도 테스트를 처음 시작할 때 예상 할 수있는 어셈블리 지침의 표현이 있습니다. 그 자체가 성능 최적화에서 중요한 교훈이 될 수 있다고 생각합니다. 컴파일러가 적용 할 수있는 영리성 측면에서 코드에서 구체적인 상수를 대체하는 변수의 차이점을 확인할 수 있습니다. shift-multiply 치환과 같은 미세 최적화는 컴파일러가 일반적으로 쉽게 수행 할 수있는 매우 낮은 수준의 최적화입니다. 성능에 훨씬 더 영향을 미치는 다른 최적화에는 에는 코드 의도를 합니다.종종 컴파일러가 액세스 할 수 없거나 일부 휴리스틱으로 추측 할 수 있습니다. 소프트웨어 엔지니어로서 당신이 여기에 왔으며 분명히 일반적으로 교대와 곱셈을 대체하지는 않습니다. I / O를 생성하고 프로세스를 차단할 수있는 서비스에 대한 중복 호출을 피하는 것과 같은 요소가 포함됩니다. 이미 메모리에있는 데이터에서 파생 된 일부 추가 데이터를 하드 디스크 나 원격 데이터베이스로 이동하는 경우 대기 시간이 백만 개의 명령 실행보다 중요합니다. 자, 우리는 당신의 원래 질문에서 조금 벗어난 것으로 생각하지만, 특히 우리가 막 번역 및 코드 실행에 대해 이해하기 시작한 누군가를 가정한다면, 이것을 질문자에게 지적한다고 생각합니다.

그렇다면 어느 것이 더 빠를까요? 실제로 성능 차이를 테스트하기 위해 선택한 좋은 방법이라고 생각합니다. 일반적으로 일부 코드 변경의 런타임 성능에 놀라게됩니다. 최신 프로세서가 사용하는 많은 기술이 있으며 소프트웨어 간의 상호 작용도 복잡 할 수 있습니다. 한 상황에서 특정 변경에 대해 유익한 성능 결과를 얻어야하더라도 이러한 유형의 변경이 항상 성능 이점을 제공한다고 결론을 내리는 것은 위험하다고 생각합니다. 그런 테스트를 한 번 실행하는 것이 위험하다고 생각합니다. "알겠습니다. 이제 어느 것이 더 빠른지 알고 있습니다!" 그런 다음 측정을 반복하지 않고 동일한 최적화를 생산 코드에 무차별 적으로 적용합니다.

따라서 이동이 곱셈보다 빠르면 어떻게해야합니까? 왜 그런지 분명히 알 수 있습니다. 위에서 볼 수 있듯이 GCC는 다른 명령어를 선호하여 직접 곱셈을 피하는 것이 좋습니다. 인텔 64 및 IA-32 아키텍처 최적화 참조 설명서는 당신에게 CPU 명령의 상대적 비용의 아이디어를 줄 것이다. 명령 대기 시간 및 처리량에보다 중점을 둔 또 다른 리소스는 http://www.agner.org/optimize/instruction_tables.pdf .. 그것들은 절대 런타임의 좋은 예표가 아니라 서로에 대한 명령의 성능을 나타냅니다. 긴밀한 루프에서 테스트를 시뮬레이션 할 때 "처리량"메트릭이 가장 관련성이 있어야합니다. 주어진 명령을 실행할 때 일반적으로 실행 단위가 묶이는주기 수입니다.

따라서 이동이 곱셈보다 빠르지 않으면 어떻게 될까요? 위에서 말했듯이, 현대 아키텍처는 상당히 복잡 할 수 있으며 분기 예측, 캐싱, 파이프 라이닝 및 병렬 실행 단위와 같은 것들이 논리적으로 동등한 두 개의 코드 조각의 상대적 성능을 때때로 예측하기 어렵게 만들 수 있습니다. 나는 이것을 강조하고 싶습니다. 왜냐하면 이것은 이것과 같은 질문에 대한 대부분의 답변과 사람들의 캠프가 변화가 곱셈보다 빠르다는 것이 더 이상 사실이 아니라고 말하는 사람들의 캠프에 만족하지 않기 때문입니다.

아는 한, 1970 년대에 비밀 엔지니어링 소스를 발명하지 않았거나 곱셈 장치와 비트 시프터의 비용 ​​차이를 갑자기 무효화 할 때마다 말입니다. 논리 게이트와 논리 연산에 대한 일반적인 곱셈은 많은 아키텍처에서 많은 시나리오에서 배럴 시프터를 사용하는 것보다 여전히 복잡합니다. 이것이 데스크톱 컴퓨터에서 전체 런타임으로 변환되는 방식이 약간 불투명 할 수 있습니다. 특정 프로세서에서 어떻게 구현되는지 잘 모르겠지만 여기에 곱셈에 대한 설명 이 있습니다. 정수 곱셈은 현대 CPU의 추가와 실제로 같은 속도입니까?

여기에 배럴 시프터에 대한 설명이 있습니다. 이전 단락에서 언급 한 문서는 CPU 명령 프록시를 통해 상대적인 운영 비용에 대한 또 다른 견해를 제공합니다. 인텔 엔지니어들은 종종 비슷한 질문을하는 것 같습니다. 인텔 개발자 영역 포럼 에서는 정수 곱셈 및 코어 2 듀오 프로세서의 추가를위한 클럭 사이클

그렇습니다. 대부분의 실제 시나리오와 거의 확실하게 JavaScript에서 성능을 위해이 동등성을 악용하려는 시도는 쓸데없는 일입니다. 그러나 곱셈 명령어를 강제로 사용하고 런타임에 차이가없는 경우에도 비용 차이가 없기 때문에 사용 된 비용 메트릭의 특성으로 인해 더욱 정확합니다. 엔드-투-엔드 런타임은 하나의 지표이며 그것이 우리가 관심을 갖는 유일한 지표라면 모든 것이 좋습니다. 그러나 이것이 곱셈과 이동의 모든 비용 차이가 단순히 사라 졌다는 것을 의미하지는 않습니다. 그리고 나는 현대 코드의 런타임 및 비용과 관련된 요소에 대한 아이디어를 얻기 시작한 암시 적 또는 다른 방법으로 질문자에게 그 아이디어를 전달하는 것은 좋은 생각이 아니라고 생각합니다. 엔지니어링은 항상 트레이드 오프에 관한 것입니다. 현대 프로세서가 사용자가 보게 될 실행 시간을 보여주기 위해 어떤 트레이드 오프를했는지에 대한 문의 및 설명은보다 차별화 된 답변을 얻을 수 있습니다. 그리고 "최적화"의 본질에 대한보다 일반적인 이해가 필요하기 때문에, 더 적은 수의 엔지니어가 가독성을 없애는 마이크로 최적화 된 코드를 확인하려는 경우 "이것은 더 이상 사실이 아닙니다"보다 더 차별화 된 대답이 필요하다고 생각합니다. 단순히 특정 인스턴스를 오래된 것으로 언급하는 것보다 다양하고 다양한 화신을 발견하십시오.


6

당신이 보는 것은 옵티마이 저의 효과입니다.

최적화 프로그램의 작업은 결과 컴파일 된 코드를 더 작게 또는 더 빠르게 만드는 것입니다.

원칙적으로, 곱셈 라이브러리에 대한 호출, 또는 종종 하드웨어 멀티 플라이어 사용은 비트 단위 시프트보다 느립니다.

따라서 ... 순진한 컴파일러가 * 2 연산을 위해 라이브러리에 대한 호출을 생성 한 경우 물론 비트 단위 시프트 *보다 느리게 실행됩니다.

그러나 옵티마이 저는 패턴을 감지하고 코드를 더 작게 / 빠르게 / 어떻게 만드는지 알아낼 수 있습니다. 그리고 당신이 본 것은 컴파일러가 * 2가 시프트와 같다는 것을 감지하는 것입니다.

관심있는 문제처럼 오늘 * 5와 같은 일부 연산에 대해 생성 된 어셈블러를 살펴 보았습니다. 실제로는 그 것이 아니라 다른 것들을보고 컴파일러가 * 5를 다음과 같이 바꿨다는 것을 알았습니다.

  • 시프트
  • 시프트
  • 원래 번호 추가

따라서 컴파일러의 최적화 프로그램은 (적어도 특정 작은 상수의 경우) 인라인 시프트를 생성하고 범용 다중 라이브러리를 호출하는 대신 추가 할만 큼 똑똑했습니다.

컴파일러 옵티마이 저의 기술은 완전히 별개의 주제이며, 마법으로 가득 차 있으며 지구상의 약 6 명이 실제로 올바르게 이해합니다. :)


3

다음과 같이 타이밍을 시도하십시오.

for (runs = 0; runs < 100000000; runs++) {
      ;
}

컴파일러는 test루프가 반복 될 때마다 값 이 변경되지 않고 최종 값 test이 사용되지 않고 루프가 완전히 제거 되었음을 인식해야합니다 .


2

곱셈은 ​​교대와 덧셈의 조합입니다.

당신이 언급 한 경우에, 컴파일러가 그것을 최적화하는지 아닌지는 중요하지 않다고 생각합니다- "곱하기 x2"는 다음 중 하나로 구현 될 수 있습니다 :

  • x한 자리의 비트를 왼쪽으로 이동합니다.
  • 추가 xx.

이것들은 각각 기본 원자 연산입니다. 하나는 다른 것보다 빠르지 않습니다.

" x4 곱하기 "(또는 임의 2^k, k>1)로 변경하면 조금 다릅니다.

  • x두 자리의 비트를 왼쪽으로 이동합니다.
  • 추가 xx하고 전화를 y추가 yy.

기본 아키텍처에, 그것의 단순한는 변화가 더 효율적이라고 볼 수 - 한 대 두 작업을 가지고, 우리는 추가 할 수 없습니다 이후 yy우리가 무엇을 알고있을 때까지 y입니다.

후자 (또는 2^k, k>1)를 구현에서 동일한 것으로 최적화하지 못하게하는 적절한 옵션을 사용하십시오. O(1)반복 추가에 비해 이동 속도가 더 빠릅니다 .O(k) .

곱셈이 2의 거듭 제곱이 아닌 경우에는 쉬프트와 덧셈의 조합 (하나가 0이 아닌 경우)이 필요합니다.


1
"기본 원자 작업"이란 무엇입니까? 교대에서 모든 비트에 병렬로 연산을 적용 할 수 있으며 가장 왼쪽 비트는 다른 비트에 의존한다고 주장 할 수 없습니까?
Bergi

2
@ Bergi : 시프트와 추가 모두 단일 기계 명령어임을 의미한다고 생각합니다. 각각의 사이클 수를 보려면 명령어 세트 문서를 살펴 봐야하지만, 추가는 종종 다중 사이클 작업이지만 시프트는 일반적으로 단일 사이클로 수행됩니다.
TMN

그렇습니다. 그러나 곱셈은 단일 머신 명령어이기도합니다 (물론 더 많은 사이클이 필요할 수도 있습니다)
Bergi

@ Bergi, 그것도 아치에 달려 있습니다. 32 비트 추가 (또는 해당되는 경우 x 비트)보다 적은 주기로 이러한 변화에 대해 어떤 생각을하고 있습니까?
OJFord

특정 아키텍처를 알지 못합니다. (그리고 컴퓨터 공학 과정이 사라졌습니다.) 아마도 두 명령 모두 한 사이클 미만이 소요될 것입니다. 아마도 시프트가 더 싼 마이크로 코드 또는 논리 게이트와 관련하여 생각했을 것입니다.
Bergi

1

부호있는 또는 부호없는 값에 2의 거듭 제곱을 곱하는 것은 왼쪽 이동과 동일하며 대부분의 컴파일러가 대체를합니다. 부호없는 값 또는 컴파일러가 음수로 증명할 수있는 부호있는 값의 구분은 오른쪽 이동과 동일하며 대부분의 컴파일러는 대체를 수행합니다. .

그러나 음의 부호있는 값을 나누는 것은 오른쪽 이동과 같지 않습니다 . 와 같은 표현식은와 (x+8)>>4동일하지 않습니다 (x+8)/16. 전자는 99 %의 컴파일러에서 -24 ~ -9 ~ -1, -8 ~ +7 ~ 0 및 +8 ~ +23 ~ 1의 값을 반올림합니다. 후자는 -39 ~ -24 ~ -1, -23 ~ +7 ~ 0, +8 ~ +23 ~ +1 [매우 비대칭이며 의도하지 않은 것]을 매핑합니다. 값이 음수가 아닌 것으로 예상 되더라도 컴파일러에서 값이 음수가 될 수 없음을 증명할 수없는 경우 >>4보다 코드를 사용 하면 코드가 더 빠릅니다 /16.


0

방금 확인한 정보가 더 있습니다.

x86_64에서 MUL opcode는 10주기 지연 및 1/2주기 처리량을 갖습니다. MOV, ADD 및 SHL의 대기 시간은 1 사이클이며 2.5, 2.5 및 1.7 사이클 처리량입니다.

15를 곱하면 최소한 3 개의 SHL과 3 개의 ADD 연산이 필요하고 아마도 몇 개의 MOV가 필요할 것입니다.

https://gmplib.org/~tege/x86-timing.pdf


0

방법론에 결함이 있습니다. 루프 증가 및 상태 확인 자체에 많은 시간이 걸립니다.

  • 빈 루프를 실행하고 시간을 측정하십시오 (호출 base).
  • 이제 1 교대 조작을 추가하고 시간을 측정하십시오 (호출 s1).
  • 다음으로 10 개의 시프트 연산을 추가하고 시간을 측정하십시오 (호출 s2).

모든 것이 올바르게 진행되면 base-s2보다 10 배 이상 높아야 base-s1합니다. 그렇지 않으면 다른 무언가가 여기에서 시작됩니다.

이제 실제로 이것을 직접 시도하고 루프가 문제를 일으키는 경우 완전히 제거하지 않는 이유를 알았습니다. 그래서 나는 계속해서 이것을했다 :

int main(){

    int test = 2;
    clock_t launch = clock();

    test << 6;
    test << 6;
    test << 6;
    test << 6;
    //.... 1 million times
    test << 6;

    clock_t done = clock();
    printf("Time taken : %d\n", done - launch);
    return 0;
}

그리고 당신은 당신의 결과를 가지고

1 밀리 초 미만의 시간 동안 백만 개의 교대 조작? .

나는 곱셈에 대해 64를 똑같이했고 같은 결과를 얻었습니다. 따라서 다른 사람들이 언급했듯이 컴파일러는 연산을 완전히 무시하고있을 것입니다.

Shiftwise 연산자 결과

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.