C에서 시프트 연산자를 사용한 곱셈과 나눗셈이 실제로 더 빠릅니까?


288

비트 연산자를 사용하여 곱셈과 나눗셈을 수행 할 수 있습니다 (예 :

i*2 = i<<1
i*3 = (i<<1) + i;
i*10 = (i<<3) + (i<<1)

등등.

실제로 직접 (i<<3)+(i<<1)사용 i*10하는 것보다 10을 곱하는 것이 사용하는 것이 더 빠릅 니까? 이런 식으로 곱하거나 나눌 수없는 입력이 있습니까?


8
실제로, 2의 거듭 제곱 이외의 상수로 값을 나누는 값이 싼 나눗셈도 가능하지만 질문에 "/ Division… / divided"로 정의를 수행하지 않는 까다로운 서브 제트입니다. 예를 들어 hackersdelight.org/divcMore.pdf를 참조하십시오 (또는 가능하면 "해커의 기쁨"책을 받으십시오).
Pascal Cuoq 2016 년

46
쉽게 테스트 할 수있는 것 같습니다.
juanchopanza 2016 년

25
평소와 같이-그것은 달려 있습니다. 옛날 옛적에 나는 곱셈이 bazillion 시계를 취하는 Intel 8088 (IBM PC / XT)의 어셈블러에서 이것을 시도했다. Shift 및 추가가 훨씬 빠르게 실행되므로 좋은 생각처럼 보입니다. 그러나 곱하는 동안 버스 장치는 명령 대기열을 자유롭게 채웠으며 다음 명령은 즉시 시작할 수 있습니다. 일련의 시프트 및 추가 후 명령 대기열이 비어 있으며 CPU는 다음 명령이 메모리에서 한 번에 1 바이트 씩 페치 될 때까지 기다려야합니다. 측정, 측정, 측정!
Bo Persson 2016 년

19
또한 오른쪽 이동은 부호없는 정수에 대해서만 잘 정의되어 있습니다. 부호있는 정수가 있으면 왼쪽에서 0 또는 가장 높은 비트가 채워지는지 여부가 정의되지 않습니다. (그리고 1 년 후 다른 사람 (심지어 자신도)이 코드를 읽는 데 걸리는 시간을 잊지 마십시오!)
Kerrek SB

29
실제로, 최적의 최적화 컴파일러는 곱셈과 나눗셈이 더 빠를 때 교대와 함께 구현합니다.
피터 G.

답변:


487

짧은 대답 : 가능성이 없습니다.

긴 대답 : 컴파일러에는 대상 프로세서 아키텍처가 가능한 한 빨리 곱하는 방법을 알고있는 최적화 프로그램이 있습니다. 최선의 방법은 컴파일러에게 의도를 명확하게 알리고 (즉, i << 1 대신 i * 2) 가장 빠른 어셈블리 / 머신 코드 시퀀스를 결정하도록하는 것입니다. 프로세서 자체가 곱셈 명령을 일련의 시프트 및 추가 마이크로 코드로 구현했을 수도 있습니다.

결론은 이것에 대해 걱정하는 데 많은 시간을 소비하지 마십시오. 이동하려는 경우 이동하십시오. 곱하기로한다면 곱하십시오. 의미 상 가장 명확한 것을 수행하십시오. 동료가 나중에 감사 할 것입니다. 아니면 다른 방법으로 나중에 저주 할 가능성이 높습니다.


31
그러나 거의 모든 응용 프로그램에서 얻을 수있는 이익은 도입 된 모호성을 완전히 능가 할 것입니다. 이런 종류의 최적화에 대해 걱정하지 마십시오. 의미가 분명한 것을 구축하고 병목 현상을 식별 한 다음 거기서 최적화하십시오.
Dave

4
가독성과 유지 관리 성을 위해 합의하면 프로파일 러 가 핫 코드 경로라고 말하는 것을 실제로 최적화하는 데 더 많은 시간을 할애하게 될 것입니다 .
doug65536

5
이 의견은 컴파일러에게 작업 수행 방법을 알려주는 잠재적 성능을 포기하는 것처럼 들립니다. 이다 없는 경우. 실제로 시프트 버전보다 x86 에서 더 나은 코드를 얻습니다 . 컴파일러 출력을 많이 보는 사람 (많은 asm / 최적화 답변 참조)으로 놀라지 않습니다. 컴파일러가 작업을 수행하는 한 가지 방법으로 손을 잡는 데 도움 이되는 경우가 있지만 이는 그중 하나가 아닙니다. gcc는 정수 수학에 중요합니다. 왜냐하면 중요하기 때문입니다. gcc -O3return i*10
Peter Cordes

다음과 같은 arduino 스케치를 다운로드했습니다 millis() >> 2. 그냥 나누기를 요청하는 것이 너무 많았 을까요?
Paul Wieland 2016 년

1
최적화 -O3으로 cortex-a9 (하드웨어 구분이 없음)에 대해 gcc에서 i / 32vs i >> 5i / 4vs i >> 2를 테스트 했으며 결과 어셈블리는 정확히 동일했습니다. 나는 나누기를 먼저 사용하는 것을 좋아하지 않았지만 내 의도를 설명하고 출력은 동일합니다.
robsn

91

몇 년 전, 해싱 알고리즘의 두 가지 버전을 벤치마킹했습니다.

unsigned
hash( char const* s )
{
    unsigned h = 0;
    while ( *s != '\0' ) {
        h = 127 * h + (unsigned char)*s;
        ++ s;
    }
    return h;
}

unsigned
hash( char const* s )
{
    unsigned h = 0;
    while ( *s != '\0' ) {
        h = (h << 7) - h + (unsigned char)*s;
        ++ s;
    }
    return h;
}

내가 벤치마킹 한 모든 머신에서 첫 번째 머신은 두 번째 머신보다 빠릅니다. 다소 놀랍게도 때로는 더 빨랐습니다 (예 : Sun Sparc). 하드웨어가 빠른 곱셈을 지원하지 않았을 때 (그리고 대부분은 그렇지 않았을 때), 컴파일러는 곱셈을 적절한 시프트와 추가 / 서브 조합으로 변환합니다. 또한 최종 목표를 알고 있기 때문에 교대조와 추가 / 서브를 명시 적으로 작성했을 때보 다 적은 지시로 수행 할 수 있습니다.

이것은 15 년 전과 비슷했습니다. 다행히도 컴파일러는 그 이후로 더 나아 졌기 때문에 컴파일러가 올바른 일을 할 수있을 것입니다. (또한 코드가 C'ish처럼 보이는 이유는 15 년 전에 사용 되었기 때문 std::string입니다. 오늘날 분명히 반복자를 사용 하고 있습니다.)


5
다음 블로그 게시물에 관심이있을 수 있습니다. 저자는 현대의 최적화 컴파일러가 프로그래머가 수학 양식에 더 효율적으로 생각하여 실제로 가장 효율적인 명령 시퀀스를 생성하는 데 사용할 수있는 공통 패턴을 리버스 엔지니어링하는 것처럼 보입니다. . shape-of-code.coding-guidelines.com/2009/06/30/…
Pascal Cuoq 2012 년

@PascalCuoq 이것에 대해 정말 새로운 것은 없습니다. 나는 20 년 전에 Sun CC와 거의 같은 것을 발견했습니다.
James Kanze

67

여기에있는 다른 모든 좋은 대답 외에도 나누거나 곱할 때 시프트를 사용하지 않는 또 다른 이유를 지적하겠습니다. 나는 한번에 누군가가 곱셈과 덧셈의 상대적 우선 순위를 잊어 버림으로써 버그를 소개하는 것을 본 적이 없다. 유지 보수 프로그래머가 시프트를 통한 "곱하기"는 논리적 으로 곱셈이지만 구문 적 으로 곱셈과 같은 우선 순위가 아니라는 것을 잊었을 때 버그가 발생하는 것을 보았습니다 . x * 2 + zx << 1 + z매우 다르다!

숫자 로 작업하는 경우 와 같은 산술 연산자를 사용하십시오 + - * / %. 비트 배열을 작업하는 경우 비트 트위들 링 연산자를 사용하십시오 & ^ | >>. 그것들을 섞지 마십시오. 비트 twiddling과 산술이 모두있는 표현식은 발생하는 버그입니다.


5
간단한 괄호로 피할 수 있습니까?
Joel B

21
@Joel : 물론입니다. 당신이 필요하다는 것을 기억한다면. 내 요점은 당신이하는 것을 잊기 쉽다는 것입니다. "x * 2"인 것처럼 "x << 1"을 읽는 정신 습관을 가진 사람들은 <<가 곱셈과 같은 우선 순위라고 생각하는 정신 습관을 가지게됩니다.
Eric Lippert 2016 년

1
글쎄, 나는 표현 "(hi << 8) + lo"가 "hi * 256 + lo"보다 더 의도적으로 드러난다. 아마도 그것은 맛의 문제 일지 모르지만 때로는 비트 트위들 링을 쓰는 것이 더 분명합니다. 대부분의 경우에 나는 당신의 요점에 전적으로 동의합니다.
Ivan Danilov 2016 년

32
@Ivan : 그리고 "(hi << 8) | lo"가 더 명확합니다. 비트 배열의 하위 비트 를 설정해도 정수추가되지 않습니다 . 비트설정 하므로 비트를 설정하는 코드를 작성하십시오.
Eric Lippert

1
와. 전에는 이런 식으로 생각하지 않았습니다. 감사.
Ivan Danilov

50

프로세서와 컴파일러에 따라 다릅니다. 일부 컴파일러는 이미 이런 식으로 코드를 최적화하지만 다른 컴파일러는 그렇지 않습니다. 따라서이 방법으로 코드를 최적화해야 할 때마다 확인해야합니다.

필사적으로 최적화 해야하는 경우가 아니라면 어셈블리 명령이나 프로세서 사이클을 저장하기 위해 소스 코드를 스크램블하지는 않습니다.


3
대략적인 추정치 추가 : 일반적인 16 비트 프로세서 (80C166)에서 두 개의 정수를 추가하면 1-2주기, 10주기의 곱셈 및 20주기의 분할이 발생합니다. 또한 i * 10을 여러 op (최소 +1주기마다 mov)로 최적화하면 일부 동작이 작동합니다. 가장 일반적인 컴파일러 (Keil / Tasking)는 2의 거듭 제곱 / 나눗셈을 제외하고는 최적화하지 않습니다.
Jens

55
그리고 일반적으로 컴파일러는 사용자보다 코드를 더 잘 최적화합니다.
user703016

"수량"을 곱하면 곱셈 연산자가 일반적으로 더 낫지 만 부호있는 값을 2의 거듭 제곱으로 나누면 >>연산자가 빠르며 /, 부호있는 값이 음수 일 수있는 경우에는 의미 적으로도 우수합니다. x>>4생성 할 가치가 필요한 경우 보다 훨씬 명확 x < 0 ? -((-1-x)/16)-1 : x/16;하며 컴파일러가 후자의 표현을 멋진 것으로 최적화하는 방법을 상상할 수 없습니다.
supercat

38

실제로 i * 10을 직접 사용하는 것보다 (i << 3) + (i << 1)을 사용하여 10을 곱하는 것이 더 빠릅니까?

컴퓨터에있을 수도 있고 없을 수도 있습니다. 관심이 있다면 실제 사용량을 측정하십시오.

사례 연구-486에서 코어 i7까지

벤치마킹은 의미있는 작업을하기가 매우 어렵지만 몇 가지 사실을 볼 수 있습니다. 에서 http://www.penguin.cz/~literakl/intel/s.html#SALhttp://www.penguin.cz/~literakl/intel/i.html#IMUL 우리는 86 클럭 사이클의 아이디어를 얻을 산술 시프트 및 곱셈에 필요합니다. "486"(최신 목록), 32 비트 레지스터 및 즉시 실행, IMUL은 13-42 사이클 및 IDIV 44를 사용한다고 가정 해 봅시다. 각 SAL은 2가 걸리고 1이 더 해져서 일부는 함께 외형 적으로 보입니다. 승자처럼

요즘 핵심 i7과 함께 :

( http://software.intel.com/en-us/forums/showthread.php?t=61481에서 )

대기 시간은 정수 덧셈의 경우 1주기이고 정수 곱셈의 경우 3주기입니다 . http://www.intel.com/products/processor/manuals/ 에있는 "Intel® 64 및 IA-32 아키텍처 최적화 참조 매뉴얼"의 부록 C에서 대기 시간과 생각을 찾을 수 있습니다 .

(일부 인텔 블러에서)

SSE를 사용하여 Core i7은 동시 추가 및 곱하기 명령어를 발행 할 수있어 클럭주기 당 8 개의 부동 소수점 연산 (FLOP)의 최고 속도

그것은 얼마나 멀리 왔는지에 대한 아이디어를 제공합니다. 비트 시프트와 같은 최적화 퀴즈 *는 90 년대에도 심각하게 받아 들여졌습니다. 비트 시프 팅은 여전히 ​​빠르지 만, 시간에 따라 2의 power / mul / div의 경우 모든 쉬프트를 수행하고 결과를 다시 더 느리게 추가합니다. 그런 다음 지침이 많을수록 캐시 오류가 증가하고 파이프 라이닝에서 더 많은 잠재적 문제가 발생하며 임시 레지스터를 많이 사용하면 스택에서 레지스터 내용을 더 많이 저장 및 복원 할 수 있습니다. 모든 영향을 결정하기에는 너무 복잡해 지지만, 주로 부정적입니다.

소스 코드의 기능과 구현

더 일반적으로 질문에는 C 및 C ++ 태그가 지정됩니다. 3 세대 언어로서, 기본 CPU 명령어 세트의 세부 사항을 숨기도록 특별히 설계되었습니다. 언어 표준을 만족 시키려면 기본 하드웨어가 지원하지 않더라도 곱셈 및 이동 연산 (및 기타 여러 가지)을 지원해야합니다 . 이러한 경우 다른 많은 지침을 사용하여 필요한 결과를 합성해야합니다. 마찬가지로 CPU에 CPU가없고 FPU가없는 경우 부동 소수점 연산을위한 소프트웨어 지원을 제공해야합니다. 최신 CPU는 모두 지원 *하고<<따라서 이것은 이론적으로나 역사적으로 보일 수도 있지만, 중요한 것은 구현을 선택할 자유가 두 가지 방식으로 진행된다는 것입니다. CPU에 일반적인 경우 소스 코드에서 요청 된 작업을 구현하는 명령이 CPU에 있어도 컴파일러는 컴파일러가 직면 한 특정 경우에 더 좋으므로 선호하는 다른 것을 선택하십시오 .

예제 (가설 적 어셈블리 언어 사용)

source           literal approach         optimised approach
#define N 0
int x;           .word x                xor registerA, registerA
x *= N;          move x -> registerA
                 move x -> registerB
                 A = B * immediate(0)
                 store registerA -> x
  ...............do something more with x...............

배타적 또는 ( xor) 와 같은 명령어 는 소스 코드와 아무 관련이 없지만, 그 자체로 아무것도 xoring하면 모든 비트가 지워 지므로 무언가를 0으로 설정하는 데 사용될 수 있습니다. 메모리 주소를 암시하는 소스 코드는 사용중인 코드가 아닐 수 있습니다.

이러한 종류의 해킹은 컴퓨터가 사용되는 한 오랫동안 사용되었습니다. 3GL 초기에는 개발자의 보안을 유지하기 위해 컴파일러 출력이 기존 하드 코어 수동 최적화 어셈블리 언어 개발자를 만족시켜야했습니다. 생성 된 코드가 느리거나 더 장황하지 않거나 더 나쁘지 않은 커뮤니티. 컴파일러는 많은 훌륭한 최적화를 신속하게 채택했습니다. 개별 어셈블리 언어 프로그래머가 할 수있는 것보다 더 중앙 집중화 된 저장소가되었습니다.하지만 특정한 경우에 결정적인 특정 최적화를 놓칠 가능성이 항상 있습니다. 누군가가 경험을 되 찾을 때까지 컴파일러가 지시 한대로 컴파일러가하는 것처럼 무언가를 더듬고 더 나은 것을 찾아냅니다.

따라서 특정 하드웨어에서 시프트 및 추가가 여전히 더 빠르더라도 컴파일러 작성자는 안전하고 유익 할 때 정확하게 작동했을 것입니다.

유지 보수성

하드웨어가 변경되면 다시 컴파일 할 수 있고 대상 CPU를 살펴보고 또 다른 최선의 선택을하는 반면, "최적화"를 다시 방문하거나 곱셈을 사용해야하는 컴파일 환경과 변경해야하는 목록은 거의 없습니다. 현대 프로세서에서 실행될 때 코드의 속도를 늦추고있는 10 년 이상 전에 작성된 비트 2 비트가 아닌 "최적화"를 생각해보십시오.

고맙게도 GCC와 같은 우수한 컴파일러는 일반적으로 일련의 비트 시프트 및 산술을 직접 곱셈으로 대체 할 수 있으므로 (예 : ...main(...) { return (argc << 4) + (argc << 2) + argc; }-> imull $21, 8(%ebp), %eax) 코드를 수정하지 않아도 재 컴파일이 도움이 될 수 있지만 보장되지는 않습니다.

곱셈이나 나눗셈을 구현하는 이상한 비트 시프 팅 코드는 개념적으로 달성하려는 것을 훨씬 덜 표현하기 때문에 다른 개발자들은 그 점에 혼란을 겪게되며 혼란스러워하는 프로그래머는 겉보기에 정신을 회복시키기 위해 버그를 도입하거나 필수적인 것을 제거 할 가능성이 높습니다. 명백하게 유익 할 때 명백하지 않은 일만하고 문서를 잘 문서화하면 (그러나 직관적 인 다른 자료는 문서화하지 않음) 모든 사람이 더 행복해질 것입니다.

일반 솔루션과 부분 솔루션

당신은 다음과 같은 몇 가지 추가 지식을 가지고 있다면 당신의 것을 int의지는 정말에만 값을 저장한다 x, y그리고 z, 당신은 그 값에 대한 작업과 컴파일러의가없는 경우보다 더 빠르게 당신에게 당신의 결과를 얻을 몇 가지 지시 사항을 해결할 수 있습니다 그 통찰력과 모든 int가치에 적합한 구현이 필요 합니다. 예를 들어, 질문을 고려하십시오.

비트 연산자를 사용하여 곱셈과 나눗셈을 수행 할 수 있습니다 ...

곱셈을 설명하지만 나누는 방법은 어떻습니까?

int x;
x >> 1;   // divide by 2?

C ++ 표준 5.8에 따르면 :

-3- E1 >> E2의 값은 E1 오른쪽으로 이동 된 E2 비트 위치입니다. E1에 부호없는 유형이 있거나 E1에 부호있는 유형과 음수가 아닌 값이있는 경우 결과 값은 E1의 몫의 정수 부분을 제곱 E2로 올린 수량 2로 나눈 값입니다. E1에 부호있는 유형과 음수 값이있는 경우 결과 값은 구현 정의됩니다.

따라서 비트 시프트 x는 음의 경우 구현 정의 결과를 갖 습니다. 다른 머신에서 동일한 방식으로 작동하지 않을 수 있습니다. 그러나 /훨씬 더 예측 가능하게 작동합니다. ( 기계마다 다른 음수 표현을 가질 수 있으므로 표현을 구성하는 비트 수가 동일하더라도 다른 범위를 가질 수 있으므로 완벽하게 일관성 이 없을 수 있습니다 .)

당신은 "나는 상관하지 않습니다 ... int그것은 직원의 나이를 저장하고 있습니다, 그것은 결코 부정적 일 수 없습니다." 특별한 통찰력 >>이 있다면, 코드에서 명시 적으로 수행하지 않는 한 안전한 최적화가 컴파일러에 의해 전달 될 수 있습니다. 그러나 이런 종류의 통찰력을 얻지 못할 때까지는 위험 하고 거의 유용하지 않으며 같은 코드로 작업하는 다른 프로그래머는 데이터에 대한 비정상적인 기대에 대해 집에 내기를 걸 었음을 알지 못합니다. 처리 할 것입니다. "최적화"로 인해 그들에게 완전히 안전한 변화가 역효과를 낳을 수 있습니다.

이런 식으로 곱하거나 나눌 수없는 입력이 있습니까?

예 ... 위에서 언급했듯이 음수는 비트 시프 팅에 의해 "분할"될 때 구현 정의 동작을 갖습니다.


2
아주 좋은 대답입니다. Core i7 vs. 486 비교는 깨달았습니다!
Drew Hall

모든 평범한 아키텍처에서 때때로 유용한 방식 intVal>>1과 다른 의미론을 갖습니다 intVal/2. 이식 가능한 방식으로 평범한 아키텍처가 얻을 수있는 가치를 계산해야하는 경우 intVal >> 1, 표현은 좀 더 복잡하고 읽기 어려워 야하며, 실제보다 열등한 코드를 생성 할 수 있습니다 intVal >> 1.
supercat

35

방금 내 컴퓨터에서 이것을 컴파일하려고 시도했습니다.

int a = ...;
int b = a * 10;

분해하면 출력이 생성됩니다.

MOV EAX,DWORD PTR SS:[ESP+1C] ; Move a into EAX
LEA EAX,DWORD PTR DS:[EAX+EAX*4] ; Multiply by 5 without shift !
SHL EAX, 1 ; Multiply by 2 using shift

이 버전은 손쉬운 이동 및 추가 기능을 통해 수동으로 최적화 된 코드보다 빠릅니다.

당신은 정말 더 나은에 그래서 단순히 쓰기, 컴파일러 가지고 올 것입니다 무엇인지 결코 정상 곱셈을하고 어디 그에게 그가 매우 정확한 경우를 제외하고,하고자하는 방법을 최적화 할 수 알고 최적화 할 수없는 컴파일러.


1
벡터에 대한 부분을 건너 뛰었다면 큰 찬사를 받았을 것입니다. 컴파일러가 곱셈을 고칠 수 있다면 벡터가 변하지 않는 것을 볼 수도 있습니다.
Bo Persson

컴파일러는 실제로 위험한 가정을하지 않으면 서 벡터 크기가 변하지 않는다는 것을 어떻게 알 수 있습니까? 또는 동시성에 대해 들어 본 적이 없습니까?
Charles Goodwin

1
자, 잠금없이 전역 벡터를 반복합니까? 그리고 주소가 취해지지 않은 로컬 벡터를 반복하고 const 멤버 함수 만 호출합니다. 적어도 내 컴파일러는 벡터 크기가 변경되지 않는다는 것을 알고 있습니다. (그리고 곧 누군가가 채팅을 위해 우리에게 플래그를 지정할 것입니다 :-).
Bo Persson 2016 년

1
@BoPersson 마지막으로,이 모든 시간이 끝난 후, 나는 컴파일러가 최적화 할 수 없다는 진술을 제거했다 vector<T>::size(). 내 컴파일러는 아주 고대였습니다! :)
user703016

21

쉬프팅은 일반적으로 명령어 레벨에서 곱하는 것보다 훨씬 빠르지 만 조기 최적화를하는 데 시간을 낭비하고있을 수 있습니다. 컴파일러는 컴파일 타임에 이러한 최적화를 수행 할 수 있습니다. 직접 작성하면 가독성에 영향을 미치며 성능에는 영향을 미치지 않습니다. 프로필을 작성하고 병목 현상을 발견 한 경우 이와 같은 작업을 수행하는 것이 좋습니다.

실제로 '매직 디비전'으로 알려진 디비전 트릭은 실제로 엄청난 성과를 낼 수 있습니다. 다시 필요한지 확인하려면 먼저 프로파일 링해야합니다. 그러나 그것을 사용하면 동일한 부서 의미에 필요한 명령을 파악하는 데 도움이되는 유용한 프로그램이 있습니다. 예를 들면 다음과 같습니다. http://www.masm32.com/board/index.php?topic=12421.0

MASM32의 OP 스레드에서 해제 한 예 :

include ConstDiv.inc
...
mov eax,9999999
; divide eax by 100000
cdiv 100000
; edx = quotient

생성합니다 :

mov eax,9999999
mov edx,0A7C5AC47h
add eax,1
.if !CARRY?
    mul edx
.endif
shr edx,16

7
@Drew는 어떤 이유로 든 귀하의 의견이 나를 웃게하고 커피를 쏟았습니다. 감사.
asawyer

30
liking math에 대한 임의의 포럼 스레드가 없습니다. 수학을 좋아하는 사람이라면 누구나 "무작위"포럼 스레드를 생성하는 것이 얼마나 어려운지 알고 있습니다.
Joel B

1
프로파일 링하고이를 병목 현상으로 발견 하고 대안 및 프로파일을 다시 구현 한 후 최소한 10 배의 성능 이점을 얻는 경우에만 이와 같은 작업을 수행하는 것이 좋습니다 .
Lie Ryan

12

시프트 및 정수 곱셈 명령어는 대부분의 최신 CPU에서 비슷한 성능을 갖습니다. 정수 곱셈 명령어는 1980 년대에 상대적으로 느리지 만 일반적으로 더 이상 사실이 아닙니다. 정수 곱하기 명령어는 대기 시간 이 높을 수 있으므로 시프트가 선호되는 경우가 여전히있을 수 있습니다. 더 많은 실행 단위를 바쁘게 유지할 수있는 경우에 적합합니다 (두 가지 방법으로 모두 줄일 수 있음).

정수 나누기는 여전히 상대적으로 느리므로 2의 거듭 제곱으로 나누기 대신 시프트를 사용하는 것이 여전히 승리하며 대부분의 컴파일러는 이것을 최적화로 구현합니다. 그러나이 최적화가 유효하기 위해서는 배당이 서명되지 않았거나 양수인 것으로 알려 져야합니다. 마이너스 배당의 경우, 시프트와 나누기는 동일하지 않습니다!

#include <stdio.h>

int main(void)
{
    int i;

    for (i = 5; i >= -5; --i)
    {
        printf("%d / 2 = %d, %d >> 1 = %d\n", i, i / 2, i, i >> 1);
    }
    return 0;
}

산출:

5 / 2 = 2, 5 >> 1 = 2
4 / 2 = 2, 4 >> 1 = 2
3 / 2 = 1, 3 >> 1 = 1
2 / 2 = 1, 2 >> 1 = 1
1 / 2 = 0, 1 >> 1 = 0
0 / 2 = 0, 0 >> 1 = 0
-1 / 2 = 0, -1 >> 1 = -1
-2 / 2 = -1, -2 >> 1 = -1
-3 / 2 = -1, -3 >> 1 = -2
-4 / 2 = -2, -4 >> 1 = -2
-5 / 2 = -2, -5 >> 1 = -3

따라서 컴파일러를 돕고 싶다면 피제수의 변수 또는 표현식이 명시 적으로 부호가 없는지 확인하십시오.


4
정수 곱셈은 예를 들어 PlayStation 3의 PPU에서 마이크로 코딩되며 전체 파이프 라인을 정지시킵니다. 여전히 일부 플랫폼에서 정수 곱셈을 피하는 것이 좋습니다 :)
Maister

2
부호없는 곱셈을 사용하여 구현 된 방법을 컴파일러가 알고 있다고 가정하면 부호없는 많은 부분이 있습니다. 각각 몇 번의 클럭 사이클에서 하나 또는 두 번의 곱셈은 각각 40 사이클 이상의 분할과 동일한 작업을 수행 할 수 있습니다.
Olof Forshell

1
@Olof : 사실이지만 컴파일 타임 상수로 나눌 때만 유효
Paul R

4

대상 장치, 언어, 목적 등에 전적으로 의존합니다.

비디오 카드 드라이버에서 픽셀 크 런칭? 그렇습니다.

부서를위한 .NET 비즈니스 애플리케이션? 그것을 들여다 볼 이유도 없습니다.

모바일 장치 용 고성능 게임의 경우 살펴볼 가치가 있지만 더 쉬운 최적화가 수행 된 후에 만 ​​가능합니다.


2

절대적으로 필요한 경우가 아니라면 코드 의도에 곱셈 / 나눗셈이 아닌 이동이 필요하지 않으면하지 마십시오.

일반적으로 적은 수의 기계 사이클을 절약 할 수 있습니다 (또는 컴파일러가 최적화 할 항목을 더 잘 알고 있기 때문에 느슨 함). 그러나 비용은 가치가 없습니다. 실제 작업보다는 사소한 세부 사항에 시간을 소비하여 코드를 유지하는 것이 더 어려워지고 당신의 동료들은 당신을 저주 할 것입니다.

각각의 저장된주기는 몇 분의 런타임을 의미하는 고부하 계산을 위해 수행해야 할 수도 있습니다. 그러나 한 번에 한 곳을 최적화하고 매번 성능 테스트를 수행하여 실제로 더 빠른지 또는 컴파일러 논리를 위반했는지 확인해야합니다.


1

내가 일부 기계에서 아는 한 곱셈에는 최대 16 ~ 32 기계 사이클이 필요할 수 있습니다. 그래서 , 기계의 종류에 따라, bitshift 연산자는 빠른 곱하기 / 나누기보다.

그러나 특정 기계에는 곱셈 / 나눗셈에 대한 특수 명령어가 포함 된 수학 프로세서가 있습니다.


7
해당 머신을 위해 컴파일러를 작성하는 사람들도 해커 딜라이트를 읽고 그에 따라 최적화합니다.
Bo Persson 2016 년

1

Drew Hall의 답변에 동의합니다. 대답은 몇 가지 추가 메모를 사용할 수 있습니다.

대다수의 소프트웨어 개발자에게 프로세서와 컴파일러는 더 이상 문제와 관련이 없습니다. 우리 대부분은 8088과 MS-DOS를 훨씬 능가합니다. 임베디드 프로세서를 위해 여전히 개발중인 사람들에게만 관련이 있습니다 ...

내 소프트웨어 회사에서는 모든 수학에 수학 (add / sub / mul / div)을 사용해야합니다. 데이터 형식간에 변환 할 때는 Shift를 사용해야합니다 (예 : n >> 256이 아닌 n >> 8로 바이트로 줄임 .


나도 동의합니다. 나는 무의식적으로 동일한 지침을 따르지만, 공식적인 요구 사항은 없었습니다.
Drew Hall

0

부호있는 정수와 오른쪽 시프트 대 나누기의 경우 차이를 만들 수 있습니다. 음수의 경우, 이동은 음의 무한대로 반올림되고 나눗셈은 0을 향해 반올림됩니다. 물론 컴파일러는 나눗셈을 더 저렴한 것으로 변경하지만 일반적으로 나누기와 같은 반올림 동작을 갖는 것으로 변경합니다. 변수가 음수가 아니거나 단순히 그렇지 않다는 것을 증명할 수 없기 때문입니다. 케어. 따라서 숫자가 음수가 아니라는 것을 증명할 수 있거나 어떤 방식으로 반올림하지 않든 상관없이 차이를 만들 가능성이 높은 방식으로 최적화를 수행 할 수 있습니다.


번호를unsigned
Lie Ryan

4
변속 동작이 표준화 되었습니까? 부정적인 정수에 대한 오른쪽 이동이 구현 정의되어 있다는 인상을 받았습니다.
Kerrek SB 2016 년

1
오른쪽 이동 음수에 대한 특정 동작에 의존하는 코드가 해당 요구 사항을 문서화해야한다는 점을 언급해야하지만 오른쪽 이동의 이점은 자연스럽게 올바른 값을 산출하고 나누기 연산자가 낭비하는 코드를 생성하는 경우에는 엄청납니다 그런 다음 사용자 코드가 원하지 않는 값을 계산하면 처음에 교대 할 내용을 산출하기 위해 조정하는 데 추가 시간을 낭비해야합니다. 사실, 만약 내가
진실을 알고

1
... 피연산자가 양수임을 알고있는 코드는 나누기 전에 부호가없는 것으로 캐스팅 된 경우 (나중에 부호가있는 부호로 다시 캐스팅하는 경우) 최적화를 향상시킬 수 있으며 피연산자가 음수 일 수 있음을 알고있는 코드는 일반적으로 해당 경우를 명시 적으로 처리해야합니다 (어떤 경우 든 그들도 긍정적이라고 가정 할 수 있습니다).
supercat

0

같은 난수에 대해 1 억 번 동일한 곱셈을 수행하는 Python 테스트.

>>> from timeit import timeit
>>> setup_str = 'import scipy; from scipy import random; scipy.random.seed(0)'
>>> N = 10*1000*1000
>>> timeit('x=random.randint(65536);', setup=setup_str, number=N)
1.894096851348877 # Time from generating the random #s and no opperati

>>> timeit('x=random.randint(65536); x*2', setup=setup_str, number=N)
2.2799630165100098
>>> timeit('x=random.randint(65536); x << 1', setup=setup_str, number=N)
2.2616429328918457

>>> timeit('x=random.randint(65536); x*10', setup=setup_str, number=N)
2.2799630165100098
>>> timeit('x=random.randint(65536); (x << 3) + (x<<1)', setup=setup_str, number=N)
2.9485139846801758

>>> timeit('x=random.randint(65536); x // 2', setup=setup_str, number=N)
2.490908145904541
>>> timeit('x=random.randint(65536); x / 2', setup=setup_str, number=N)
2.4757170677185059
>>> timeit('x=random.randint(65536); x >> 1', setup=setup_str, number=N)
2.2316000461578369

따라서 파이썬에서 곱셈 / 나눗셈보다 2의 거듭 제곱보다 시프트를 수행하면 약간 개선됩니다 (나눗셈의 경우 ~ 10 %, 곱하기의 경우 ~ 1 %). 2의 제곱이 아닌 경우 상당한 속도 저하가있을 수 있습니다.

이 #도 프로세서, 컴파일러 (또는 인터프리터-간결함을 위해 파이썬에서 한 것)에 따라 변경됩니다.

다른 모든 사람들과 마찬가지로 조기 최적화하지 마십시오. 읽을 수있는 코드를 작성하고, 빠르지 않은 경우 프로파일을 작성한 다음 느린 부분을 최적화하십시오. 컴파일러는 최적화하는 것보다 훨씬 좋습니다.


0

컴파일러는 입력 집합이 축소 된 경우에만 작동하기 때문에 컴파일러가 수행 할 수없는 최적화가 있습니다.

아래에는 64 비트 "역수 곱셈"을 수행하는 더 빠른 나눗셈을 수행 할 수있는 c ++ 샘플 코드가 있습니다. 분자와 분모 모두 특정 임계 값 미만이어야합니다. 실제로 64 비트 명령어를 사용하여 실제로 정규 나누기보다 빠르도록 컴파일해야합니다.

#include <stdio.h>
#include <chrono>

static const unsigned s_bc = 32;
static const unsigned long long s_p = 1ULL << s_bc;
static const unsigned long long s_hp = s_p / 2;

static unsigned long long s_f;
static unsigned long long s_fr;

static void fastDivInitialize(const unsigned d)
{
    s_f = s_p / d;
    s_fr = s_f * (s_p - (s_f * d));
}

static unsigned fastDiv(const unsigned n)
{
    return (s_f * n + ((s_fr * n + s_hp) >> s_bc)) >> s_bc;
}

static bool fastDivCheck(const unsigned n, const unsigned d)
{
    // 32 to 64 cycles latency on modern cpus
    const unsigned expected = n / d;

    // At least 10 cycles latency on modern cpus
    const unsigned result = fastDiv(n);

    if (result != expected)
    {
        printf("Failed for: %u/%u != %u\n", n, d, expected);
        return false;
    }

    return true;
}

int main()
{
    unsigned result = 0;

    // Make sure to verify it works for your expected set of inputs
    const unsigned MAX_N = 65535;
    const unsigned MAX_D = 40000;

    const double ONE_SECOND_COUNT = 1000000000.0;

    auto t0 = std::chrono::steady_clock::now();
    unsigned count = 0;
    printf("Verifying...\n");
    for (unsigned d = 1; d <= MAX_D; ++d)
    {
        fastDivInitialize(d);
        for (unsigned n = 0; n <= MAX_N; ++n)
        {
            count += !fastDivCheck(n, d);
        }
    }
    auto t1 = std::chrono::steady_clock::now();
    printf("Errors: %u / %u (%.4fs)\n", count, MAX_D * (MAX_N + 1), (t1 - t0).count() / ONE_SECOND_COUNT);

    t0 = t1;
    for (unsigned d = 1; d <= MAX_D; ++d)
    {
        fastDivInitialize(d);
        for (unsigned n = 0; n <= MAX_N; ++n)
        {
            result += fastDiv(n);
        }
    }
    t1 = std::chrono::steady_clock::now();
    printf("Fast division time: %.4fs\n", (t1 - t0).count() / ONE_SECOND_COUNT);

    t0 = t1;
    count = 0;
    for (unsigned d = 1; d <= MAX_D; ++d)
    {
        for (unsigned n = 0; n <= MAX_N; ++n)
        {
            result += n / d;
        }
    }
    t1 = std::chrono::steady_clock::now();
    printf("Normal division time: %.4fs\n", (t1 - t0).count() / ONE_SECOND_COUNT);

    getchar();
    return result;
}

0

한 경우에는 2의 거듭 제곱으로 나누거나 나누려는 경우 컴파일러가 비트 시프트 연산자를 MUL / DIV로 변환하더라도 비트 시프트 연산자를 사용하면 잘못 될 수 없다고 생각합니다. 매크로) 어쨌든, 이러한 경우 특히 시프트가 1 이상인 경우 개선이 이루어집니다. 또는 더 명확하게 말하면 CPU에 비트 시프트 연산자가 없으면 MUL / DIV가되지만 CPU는 비트 시프트 연산자를 사용하면 마이크로 코드 분기를 피할 수 있으며 이는 명령 수가 적습니다.

조밀 한 이진 트리에서 작동하기 때문에 많은 배가 / 반으로 작동 해야하는 코드를 작성 중이며 왼쪽보다 더 나은 것으로 생각되는 작업이 하나 더 있습니다. )를 추가하여 이동합니다. 왼쪽 시프트와 xor로 대체 할 수 있습니다. 시프트가 추가하려는 비트 수보다 더 넓은 경우 쉬운 예는 (i << 1) ^ 1이며, 1을 두 배로 늘립니다. 왼쪽 (작은 엔디안) 시프트 만 간격을 0으로 채우므로 오른쪽 시프트 (2 나누기의 거듭 제곱)에는 적용되지 않습니다.

내 코드 에서이 곱하기 / 나누기 2와 두 연산의 힘은 매우 집중적으로 사용되며 수식이 이미 짧기 때문에 제거 할 수있는 각 명령이 상당한 이득이 될 수 있습니다. 프로세서가 이러한 비트 시프트 연산자를 지원하지 않으면 이득은 발생하지 않지만 손실은 발생하지 않습니다.

또한, 내가 쓰고있는 알고리즘에서 그것들은 실제로 더 명확하다는 의미에서 발생하는 움직임을 시각적으로 나타냅니다. 이진 트리의 왼쪽은 더 크고 오른쪽은 더 작습니다. 또한 내 코드에서 홀수 및 짝수에는 특별한 의미가 있으며, 트리의 모든 왼손잡이는 홀수이고 모든 오른 손잡이 및 루트는 짝수입니다. 어떤 경우에는 아직 접하지 않았지만 실제로는 이것을 생각조차하지 않았지만 x & 1이 x % 2에 비해 더 최적의 작업 일 수 있습니다. 짝수의 x & 1은 0을 생성하지만 홀수에 대해서는 1을 생성합니다.

x & 3에 대해 0을 얻는다면 4가 우리의 수의 요소이며 8의 경우 x % 7과 동일하다는 것을 알고 있습니다. 이러한 경우에는 유틸리티가 제한적이지만 비트 연산은 거의 항상 가장 빠르며 컴파일러에게는 모호 할 가능성이 거의 없기 때문에 모듈러스 연산을 피하고 비트 논리 연산을 대신 사용할 수 있다는 것을 알고 있습니다.

나는 밀도가 높은 이진 트리 필드를 거의 발명했기 때문에 사람들 이이 의견의 가치를 파악하지 못할 것으로 기대합니다. 사람들은 단지 2의 거듭 제곱에서만 인수 분해를 수행하거나 2의 곱셈 / 나눗셈 만 수행하기를 원하지 않기 때문입니다.



0

gcc 컴파일러에서 x + x, x * 2 및 x << 1 구문의 출력을 비교하면 x86 어셈블리에서 동일한 결과를 얻을 수 있습니다 : https://godbolt.org/z/JLpp0j

        push    rbp
        mov     rbp, rsp
        mov     DWORD PTR [rbp-4], edi
        mov     eax, DWORD PTR [rbp-4]
        add     eax, eax
        pop     rbp
        ret

따라서 gcc 를 입력 한 내용과 독립적으로 자신의 최상의 솔루션을 결정하기에 충분히 똑똑 하다고 생각할 수 있습니다 .


0

나도 집을 이길 수 있는지 알고 싶었다. 이것은 숫자의 곱셈에 의한 모든 숫자에 대한 비트 단위입니다. 내가 만든 매크로는 일반 곱셈보다 약 25 % 더 두 배 느립니다. 2의 배수에 가깝거나 2의 배수로 구성된 경우 다른 사람들이 말했듯이 이길 수 있습니다. (X << 4) + (X << 2) + (X << 1) + X로 구성된 X * 23과 같이 (X << 6) + X로 구성된 X * 65는 느려집니다.

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

#define MULTIPLYINTBYMINUS(X,Y) (-((X >> 30) & 1)&(Y<<30))+(-((X >> 29) & 1)&(Y<<29))+(-((X >> 28) & 1)&(Y<<28))+(-((X >> 27) & 1)&(Y<<27))+(-((X >> 26) & 1)&(Y<<26))+(-((X >> 25) & 1)&(Y<<25))+(-((X >> 24) & 1)&(Y<<24))+(-((X >> 23) & 1)&(Y<<23))+(-((X >> 22) & 1)&(Y<<22))+(-((X >> 21) & 1)&(Y<<21))+(-((X >> 20) & 1)&(Y<<20))+(-((X >> 19) & 1)&(Y<<19))+(-((X >> 18) & 1)&(Y<<18))+(-((X >> 17) & 1)&(Y<<17))+(-((X >> 16) & 1)&(Y<<16))+(-((X >> 15) & 1)&(Y<<15))+(-((X >> 14) & 1)&(Y<<14))+(-((X >> 13) & 1)&(Y<<13))+(-((X >> 12) & 1)&(Y<<12))+(-((X >> 11) & 1)&(Y<<11))+(-((X >> 10) & 1)&(Y<<10))+(-((X >> 9) & 1)&(Y<<9))+(-((X >> 8) & 1)&(Y<<8))+(-((X >> 7) & 1)&(Y<<7))+(-((X >> 6) & 1)&(Y<<6))+(-((X >> 5) & 1)&(Y<<5))+(-((X >> 4) & 1)&(Y<<4))+(-((X >> 3) & 1)&(Y<<3))+(-((X >> 2) & 1)&(Y<<2))+(-((X >> 1) & 1)&(Y<<1))+(-((X >> 0) & 1)&(Y<<0))
#define MULTIPLYINTBYSHIFT(X,Y) (((((X >> 30) & 1)<<31)>>31)&(Y<<30))+(((((X >> 29) & 1)<<31)>>31)&(Y<<29))+(((((X >> 28) & 1)<<31)>>31)&(Y<<28))+(((((X >> 27) & 1)<<31)>>31)&(Y<<27))+(((((X >> 26) & 1)<<31)>>31)&(Y<<26))+(((((X >> 25) & 1)<<31)>>31)&(Y<<25))+(((((X >> 24) & 1)<<31)>>31)&(Y<<24))+(((((X >> 23) & 1)<<31)>>31)&(Y<<23))+(((((X >> 22) & 1)<<31)>>31)&(Y<<22))+(((((X >> 21) & 1)<<31)>>31)&(Y<<21))+(((((X >> 20) & 1)<<31)>>31)&(Y<<20))+(((((X >> 19) & 1)<<31)>>31)&(Y<<19))+(((((X >> 18) & 1)<<31)>>31)&(Y<<18))+(((((X >> 17) & 1)<<31)>>31)&(Y<<17))+(((((X >> 16) & 1)<<31)>>31)&(Y<<16))+(((((X >> 15) & 1)<<31)>>31)&(Y<<15))+(((((X >> 14) & 1)<<31)>>31)&(Y<<14))+(((((X >> 13) & 1)<<31)>>31)&(Y<<13))+(((((X >> 12) & 1)<<31)>>31)&(Y<<12))+(((((X >> 11) & 1)<<31)>>31)&(Y<<11))+(((((X >> 10) & 1)<<31)>>31)&(Y<<10))+(((((X >> 9) & 1)<<31)>>31)&(Y<<9))+(((((X >> 8) & 1)<<31)>>31)&(Y<<8))+(((((X >> 7) & 1)<<31)>>31)&(Y<<7))+(((((X >> 6) & 1)<<31)>>31)&(Y<<6))+(((((X >> 5) & 1)<<31)>>31)&(Y<<5))+(((((X >> 4) & 1)<<31)>>31)&(Y<<4))+(((((X >> 3) & 1)<<31)>>31)&(Y<<3))+(((((X >> 2) & 1)<<31)>>31)&(Y<<2))+(((((X >> 1) & 1)<<31)>>31)&(Y<<1))+(((((X >> 0) & 1)<<31)>>31)&(Y<<0))
int main()
{
    int randomnumber=23;
    int randomnumber2=23;
    int checknum=23;
    clock_t start, diff;
    srand(time(0));
    start = clock();
    for(int i=0;i<1000000;i++)
    {
        randomnumber = rand() % 10000;
        randomnumber2 = rand() % 10000;
        checknum=MULTIPLYINTBYMINUS(randomnumber,randomnumber2);
        if (checknum!=randomnumber*randomnumber2)
        {
            printf("s %i and %i and %i",checknum,randomnumber,randomnumber2);
        }
    }
    diff = clock() - start;
    int msec = diff * 1000 / CLOCKS_PER_SEC;
    printf("MULTIPLYINTBYMINUS Time %d milliseconds", msec);
    start = clock();
    for(int i=0;i<1000000;i++)
    {
        randomnumber = rand() % 10000;
        randomnumber2 = rand() % 10000;
        checknum=MULTIPLYINTBYSHIFT(randomnumber,randomnumber2);
        if (checknum!=randomnumber*randomnumber2)
        {
            printf("s %i and %i and %i",checknum,randomnumber,randomnumber2);
        }
    }
    diff = clock() - start;
    msec = diff * 1000 / CLOCKS_PER_SEC;
    printf("MULTIPLYINTBYSHIFT Time %d milliseconds", msec);
    start = clock();
    for(int i=0;i<1000000;i++)
    {
        randomnumber = rand() % 10000;
        randomnumber2 = rand() % 10000;
        checknum= randomnumber*randomnumber2;
        if (checknum!=randomnumber*randomnumber2)
        {
            printf("s %i and %i and %i",checknum,randomnumber,randomnumber2);
        }
    }
    diff = clock() - start;
    msec = diff * 1000 / CLOCKS_PER_SEC;
    printf("normal * Time %d milliseconds", msec);
    return 0;
}
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.