x << 1 또는 x << 10 중 어느 것이 더 빠릅니까?


83

나는 아무것도 최적화하고 싶지 않습니다. 맹세합니다. 호기심에서이 질문을하고 싶습니다. 나는 대부분의 하드웨어에서 비트 변화 (예를 들면의 어셈블리 명령어가 있다는 것을 알고 shl, shr하나의 명령이다). 그러나 얼마나 많은 비트를 이동하는지가 중요합니까 (나노초 단위 또는 CPU 전술 단위). 즉, 다음 중 하나가 CPU에서 더 빠릅니까?

x << 1;

x << 10;

그리고이 질문에 대해 나를 미워하지 마십시오. :)


17
Omg, 나는 코드를 훑어 보았고 나의 첫번째 생각은 "스트림 프린팅 오퍼레이터"였다. 나는 쉬는 시간이 필요합니다.
Kos

4
누군가가 "조기 최적화"라는 말을 희미하게 마음 속으로 들었거나 어쩌면 내 상상력 일 수도 있습니다.
tia

5
@tia 그는 그가 아무것도 최적화하지 않을 것이라고 말했다 :)

1
@Grigory 예, 그렇기 때문에 여기에서 그 문구로 질문을 건너 뛰는 사람을 볼 수 없습니다. : D
tia

1
참고로 저는 최근에 왼쪽으로 이동하고 오른쪽으로 이동하는 것이 반드시 동일한 CPU 시간을 소비하는 것은 아니라는 것을 인식했습니다. 제 경우에는 오른쪽으로 이동하는 것이 훨씬 느 렸습니다. : 우선은 놀라게하지만 난 대답은 논리적 왼쪽 이동 수단과 오른쪽 어쩌면 이동하는 것으로 생각되었다 산술 의미 stackoverflow.com/questions/141525/...을
기독교 Ammer

답변:


84

잠재적으로 CPU에 따라 다릅니다.

그러나 모든 최신 CPU (x86, ARM)는 "배럴 시프터"(일정한 시간에 임의의 시프트를 수행하도록 특별히 설계된 하드웨어 모듈)를 사용합니다.

그래서 결론은 ... 아뇨. 차이 없음.


21
좋아, 이제 내 머리에 배럴 롤을하도록 CPU에 명령하는 이미지가
Ignacio Vazquez-Abrams

11
Errr-프로세서에 따라 매우 다릅니다. 일부 프로세서에서는이 시간이 일정합니다. 다른 경우에는 시프트 당 한 사이클이 될 수 있습니다 (한때 프로세서 클럭 속도를 측정하는 s / w 방법으로 약 60,000 자리 시프트를 사용했습니다). 그리고 다른 프로세서에서는 단일 비트 시프트에 대한 명령 만있을 수 있습니다.이 경우 다중 비트 시프트가 반복되는 루프에있는 라이브러리 루틴에 위임됩니다.
quick_now 2010

4
@quickly_now : 확실히 클럭 속도를 측정하는 나쁜 방법입니다. 실제로 60,000 교대를 수행 할만큼 어리석은 프로세서는 없습니다. 그것은 단순히 60000 mod register_size. 예를 들어, 32 비트 프로세서는 시프트 카운트의 최하위 비트 5 개만 사용합니다.
casablanca

4
inmos 변환기에는 32 비트 피연산자가 시프트 횟수를 취하는 시프트 연산자가 있습니다. 원한다면 각각 1 클럭 씩 40 억 교대를 할 수 있습니다. "아무 프로세서도 충분히 어리 석다". 죄송합니다. 이건 해냈어. 하지만 어셈블러에서 해당 부분을 코딩해야했습니다. 컴파일러는 현명한 수정 / 최적화를 수행했습니다 (결과를 0으로 설정하고 아무것도하지 않음).
quick_now 2010

5
펜티엄 4는 슬프게도 배럴 시프터를 잃어 전반적인 클럭 당 지침 속도가 저하되었습니다. Core Blah 아키텍처가 되찾았다 고 가정합니다.
Russell Borogove

64

일부 임베디드 프로세서에는 "하나로 이동"명령 만 있습니다. 같은 프로세서에서 컴파일러는 바꿀 것 x << 3으로 ((x << 1) << 1) << 1.

모토로라 MC68HCxx는 이러한 한계가있는 가장 인기있는 제품군 중 하나라고 생각합니다. 다행히도 이러한 아키텍처는 이제 매우 드물며 대부분은 가변 시프트 크기의 배럴 시프터를 포함합니다.

많은 최신 파생물이있는 Intel 8051은 또한 임의의 비트 수를 이동할 수 없습니다.


12
임베디드 마이크로 컨트롤러에서는 여전히 일반적입니다.
Ben Jackson

4
"희귀"란 무슨 뜻입니까? 따라서 판매 된 8 비트 마이크로 컨트롤러의 수는 다른 모든 유형의 MPU보다 많습니다.
Vovanium 2010

8 비트 마이크로 컨트롤러는 더 많은 프로그램 ROM, 더 많은 작업 RAM 및 더 많은 기능을 통해 동일한 단위당 가격 (예 : TI의 MSP430)으로 16 비트를 얻을 수있는 새로운 개발에 많이 사용되지 않습니다. 일부 8 비트 마이크로 컨트롤러에도 배럴 시프터가 있습니다.
Ben Voigt

1
마이크로 컨트롤러의 단어 크기는 배럴 시프터가 있는지 여부와 관련이 없습니다. 제가 언급 한 MC68HCxx 제품군에도 16 비트 프로세서가 있으며 모두 한 번에 단일 비트 위치 만 이동합니다.
Ben Voigt

대부분의 8 비트 MCU에는 배럴 시프터가 없다는 사실은 사실이 아니며 배럴 시프터가없는 비 8 비트 MCU가 있다는 것은 맞습니다. 비트 니스는 배럴 시프터가있는 기계에 대한 신뢰할 수있는 근사치가되었습니다. 또한 MCU 용 CPU 코어는 종종 모델에 대한 선택을 설정하지 않지만 온칩 주변 장치는 적합합니다. 그리고 8 비트는 종종 같은 가격에 더 풍부한 주변 장치를 위해 선택됩니다.
Vovanium 2010

29

이것에 대한 많은 경우가 있습니다.

  1. 많은 고속 MPU에는 배럴 시프터, 멀티플렉서와 ​​같은 전자 회로가있어 일정한 시간에 어떤 시프트도 수행합니다.

  2. MPU에 1 비트 시프트 만있는 경우 x << 10일반적으로 10 시프트 또는 2 시프트로 바이트 복사로 수행되므로 일반적으로 더 느립니다.

  3. 그러나 일반적인 경우가 알려져 x << 10도 될 것 빠른 이상을 x << 1. x가 16 비트 인 경우 하위 6 비트 만주의해야합니다 (다른 모든 비트는 시프트 됨). 따라서 MPU는 하위 바이트 만로드하면되므로 8 비트 메모리에 대한 단일 액세스 주기만 만들고 x << 102 개의 액세스주기가 필요합니다. 액세스주기가 시프트보다 느리면 (하위 바이트 지우기) x << 10더 빠릅니다. 이것은 느린 외부 데이터 RAM에 액세스하는 동안 빠른 온보드 프로그램 ROM이있는 마이크로 컨트롤러에 적용될 수 있습니다.

  4. 경우 3 외에도 컴파일러는 x << 1016x16 곱셈을 16x8 1로 바꾸는 것과 같이 (하위 바이트는 항상 0이므로) 더 낮은 너비의 비트로 추가 작업을 최적화 할 수 있습니다.

일부 마이크로 컨트롤러에는 왼쪽 시프트 명령이 전혀 없으며 add x,x대신 사용 합니다.


나는 그것을 이해하지 못한다. 왜 x << 10이 x << 8보다 빠르다. 여기서 x << 8에서는 16 비트에서 더 낮은 바이트에서로드를해야하고로드와 두 번의 교대를 수행하지 않아야합니다. 이해가 안 돼요.
없음

3
@none : x << 10이 x << 8보다 빠르다고 말하지 않았습니다.
Vovanium 2010

9

ARM에서는 다른 명령어의 부작용으로이 작업을 수행 할 수 있습니다. 따라서 잠재적으로 둘 중 하나에 대해 대기 시간이 전혀 없습니다.


1
명령이 동일한 주기로 실행됩니까? 일부 아키텍처에서는 동일한 명령어가 피연산자에 따라 몇 가지 다른 연산 코드로 변환되며 1 ~ 5 사이클이 소요됩니다.
Nick T

@Nick ARM 명령어는 일반적으로 1 ~ 2 사이클이 걸립니다. 최신 아키텍처에는 확실하지 않습니다.
onemasse 2010

2
@Nick T : 그는 ARM에 대해 말하면서 전용 명령어가 아니라 많은 데이터 처리 명령어의 '특징'으로 전환했습니다. 즉 ADD R0, R1, R2 ASL #3, 3 비트 왼쪽으로 이동 한 R1 및 R2를 추가합니다.
Vovanium 2010


7

이는 CPU와 컴파일러에 따라 다릅니다. 기본 CPU에 배럴 시프터를 사용하여 임의의 비트 시프트가 있더라도 컴파일러가 해당 리소스를 활용하는 경우에만 발생합니다.

데이터의 비트 단위로 너비를 벗어난 모든 항목을 이동하는 것은 C 및 C ++에서 "정의되지 않은 동작"입니다. 서명 된 데이터의 오른쪽 시프트도 "구현 정의"입니다. 속도에 대해 너무 많은 관심을 갖기보다는 다른 구현에 대해 동일한 답을 얻고 있다는 점을 염두에 두십시오.

ANSI C 섹션 3.3.7에서 인용 :

3.3.7 비트 시프트 연산자

통사론

      shift-expression:
              additive-expression
              shift-expression <<  additive-expression
              shift-expression >>  additive-expression

제약

각 피연산자는 정수 유형을 가져야합니다.

의미론

적분 프로모션은 각 피연산자에서 수행됩니다. 결과 유형은 승격 된 왼쪽 피연산자의 유형입니다. 오른쪽 피연산자의 값이 음수이거나 승격 된 왼쪽 피연산자의 너비 (비트)보다 크거나 같으면 동작이 정의되지 않습니다.

E1 << E2의 결과는 E1 왼쪽으로 이동 한 E2 비트 위치입니다. 비워진 비트는 0으로 채워집니다. E1에 부호없는 유형이있는 경우 결과 값은 E1에 수량을 곱하고 2를 E2 거듭 제곱하고 E1에 부호없는 long 유형이있는 경우 모듈로 ULONG_MAX + 1을 줄이고 그렇지 않으면 UINT_MAX + 1입니다. (상수 ULONG_MAX 및 UINT_MAX는 헤더에 정의되어 있습니다.)

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

그래서:

x = y << z;

"<<": y × 2 z ( 오버플로가 발생하면 정의되지 않음 );

x = y >> z;

">>": 서명에 대한 구현 정의 (대부분 산술 시프트의 결과 : y / 2 z ).


나는 1u << 100UB 라고 생각하지 않습니다 . 그것은 단지 0입니다.
Armen Tsirunyan 2010

@Armen Tsirunyan : 비트 시프트 1u << 100로서의 비트 시프트 오버플로 일 있습니다. 1u << 100산술 시프트는 0이므로 ANSI C에서는 <<비트 시프트입니다. en.wikipedia.org/wiki/Arithmetic_shift
늑대

2
@Armen Tsirunyan : ANSI 섹션 3.3.7 참조- 오른쪽 피연산자의 값이 음수이거나 승격 된 왼쪽 피연산자의 비트 너비보다 크거나 같으면 동작이 정의되지 않습니다. 따라서 101+ 비트 유형이없는 한 ANSI C 시스템의 UB는 예입니다.
늑대

@ carrot-pot : 좋아, 당신은 나를 설득했다 :)
Armen Tsirunyan 2010

관련 : x << (y & 31)(86가하는 것처럼) 컴파일러는 타겟 아키텍처의 시프트 명령 마스크 수를 알고 있다면 여전히, 아니 및 명령에 하나의 시프트 명령어로 컴파일 할 수 있습니다. (가급적이면 마스크를 하드 코딩하지 말고 다른 곳에서 가져 오십시오 CHAR_BIT * sizeof(x) - 1.) 이것은 입력에 관계없이 C UB없이 단일 명령어로 컴파일되는 회전 관용구를 작성하는 데 유용합니다. ( stackoverflow.com/questions/776508/… ).
Peter Cordes

7

8 비트 프로세서에서는 x<<1실제로 16 비트 값 보다 훨씬 느릴 수 있습니다 x<<10.

예를 들어 합리적인 번역 x<<1은 다음과 같습니다.

byte1 = (byte1 << 1) | (byte2 >> 7)
byte2 = (byte2 << 1)

반면 x<<10더 간단합니다.

byte1 = (byte2 << 2)
byte2 = 0

x<<1보다 자주 그리고 더 멀리 이동하는 것을 주목하십시오 x<<10. 또한의 결과 x<<10는 byte1의 내용에 의존하지 않습니다. 이것은 추가적으로 작업 속도를 높일 수 있습니다.


5

일부 세대의 Intel CPU (P2 또는 P3? AMD가 아님)에서는 비트 시프트 작업이 엄청나게 느립니다. 1 비트 씩 비트 시프트는 덧셈 만 사용할 수 있기 때문에 항상 빠릅니다. 고려해야 할 또 다른 질문은 일정한 비트 수에 의한 비트 시프트가 가변 길이 시프트보다 빠른지 여부입니다. opcode가 같은 속도 인 경우에도 x86에서 비트 시프트의 상수가 아닌 오른쪽 피연산자는 CL 레지스터를 차지해야하므로 레지스터 할당에 추가 제약이 부과되고 프로그램 속도도 느려질 수 있습니다.


1
이것이 바로 펜티엄 4입니다. PPro에서 파생 된 CPU (P2 및 P3와 같은)는 빠른 변화를 보입니다. 그리고 네, x86에서 가변 카운트 변화는 BMI2 사용할 수 있습니다하지 않는 한 그들이 할 수있는 것보다 느립니다 shlx/ shrx/ sarx(하 스웰 이상 및 Ryzen을). CISC 의미 (count = 0 인 경우 수정되지 않은 플래그)는 여기서 x86을 손상시킵니다. shl r32, clSandybridge 제품군에서 3 uops입니다 (Intel이 플래그 결과가 사용되지 않은 경우 uop 중 하나를 취소 할 수 있다고 Intel이 주장하지만). AMD는 단일 UOP있다 shl r32, cl(그러나 확장 정밀도 느린 더블 변화를, shld r32, r32, cl)
피터 코르

1
시프트 (가변 카운트 포함)는 P6 제품군에서 단일 uop 일 뿐이지 만 플래그 결과를 읽 shl r32, cl거나 1이 아닌 즉시 플래그를 읽으면 시프트가 끝날 때까지 프런트 엔드가 중단됩니다 ! ( stackoverflow.com/questions/36510095/… ). 컴파일러는이를 알고 test있으며 시프트의 플래그 결과를 사용하는 대신 별도의 명령어를 사용합니다. (그러나 이것은 문제가되지 않는 CPU에 대한 지침을 낭비합니다. stackoverflow.com/questions/40354978/… 참조 )
Peter Cordes

3

항상 그렇듯이 주변 코드 컨텍스트에 따라 다릅니다 . 예를 들어 x<<1배열 인덱스로 사용 하고 있습니까? 아니면 다른 것에 추가 하시겠습니까? 두 경우 모두 작은 시프트 카운트 (1 또는 2)는 컴파일러가 시프트 만하면되는 경우보다 더 많이 최적화 할 수 있습니다 . 전체 처리량 대 대기 시간 대 프런트 엔드 병목 현상의 절충은 말할 것도 없습니다. 작은 조각의 성능은 1 차원이 아닙니다.

하드웨어 시프트 명령어는 컴파일을위한 컴파일러의 유일한 옵션이 x<<1아니지만 다른 답변은 대부분이를 가정합니다.


x << 1x+x부호없는 정수와 2의 보수 부호있는 정수와 정확히 동일합니다 . 컴파일러는 컴파일하는 동안 대상 하드웨어를 항상 알고 있으므로 이와 같은 트릭을 활용할 수 있습니다.

인텔 하 스웰 , add시계 처리량 당 4를 가지고 있지만 shl즉각적인 카운트 만 2 클럭 처리량 당이있다. ( http://agner.org/optimize/ 참조 지침 표 및 기타 링크는 를 .태그 위키). SIMD 벡터 시프트는 클럭 당 1 (Skylake에서 2)이지만 SIMD 벡터 정수 추가는 클럭 당 2 (Skylake에서 3)입니다. 대기 시간은 동일하지만 1주기입니다.

또한 shlopcode에서 카운트가 암시되는 위치 에 대한 특별한 시프트 바이 1 인코딩이 있습니다. 8086에는 즉시 카운트 시프트가 없었으며 cl레지스터 별로 하나씩 만 있습니다. 메모리 피연산자를 시프트하지 않는 한 왼쪽 시프트 만 추가 할 수 있기 때문에 이는 대부분 오른쪽 시프트와 관련이 있습니다. 그러나 나중에 값이 필요하면 레지스터에 먼저로드하는 것이 좋습니다. 그러나 어쨌든 shl eax,1또는 add eax,eax보다 1 바이트 더 짧으며 shl eax,10코드 크기는 직접 (디코딩 / 프런트 엔드 병목 현상) 또는 간접적으로 (L1I 코드 캐시 누락) 성능에 영향을 미칠 수 있습니다.

보다 일반적으로, 작은 시프트 카운트는 x86의 주소 지정 모드에서 스케일링 된 인덱스로 최적화 될 수 있습니다. 요즘 일반적으로 사용되는 대부분의 다른 아키텍처는 RISC이며 확장 인덱스 주소 지정 모드가 없지만 x86은 언급 할 가치가있는 일반적인 아키텍처입니다. (예를 들어 4 바이트 요소의 배열을 인덱싱하는 경우)에 대한 배율 인수를 1 씩 늘릴 여지가 있습니다 int arr[]; arr[x<<1].


의 원래 값 x이 여전히 필요한 상황에서는 복사 + 이동이 필요합니다. 그러나 대부분의 x86 정수 명령어는 제자리에서 작동합니다. (대상은 add또는 같은 명령어의 소스 중 하나입니다 shl.) x86-64 System V 호출 규칙은 레지스터에 인수를 전달합니다. 첫 번째 인수는에 edi있고 반환 값은 eax에 있습니다. 따라서 반환하는 함수 x<<10는 컴파일러가 복사 + 시프트를 방출 하도록합니다 암호.

LEA명령어를 사용하면 시프트 및 추가를 수행 할 수 있습니다 (어드레싱 모드 머신 인코딩을 사용하기 때문에 시프트 카운트 0에서 3). 결과를 별도의 레지스터에 넣습니다.

gcc와 clang은 모두 Godbolt 컴파일러 탐색기에서 볼 수 있듯이 동일한 방식으로 이러한 함수를 최적화합니다 .

int shl1(int x) { return x<<1; }
    lea     eax, [rdi+rdi]   # 1 cycle latency, 1 uop
    ret

int shl2(int x) { return x<<2; }
    lea     eax, [4*rdi]    # longer encoding: needs a disp32 of 0 because there's no base register, only scaled-index.
    ret

int times5(int x) { return x * 5; }
    lea     eax, [rdi + 4*rdi]
    ret

int shl10(int x) { return x<<10; }
    mov     eax, edi         # 1 uop, 0 or 1 cycle latency
    shl     eax, 10          # 1 uop, 1 cycle latency
    ret

구성 요소가 2 개인 LEA는 최신 Intel 및 AMD CPU에서 1주기 지연 시간과 클럭 당 2 개의 처리량을 제공합니다. (Sandybridge 가족 및 Bulldozer / Ryzen). Intel에서는 클럭 처리량 당 1 개이며 lea eax, [rdi + rsi + 123]. (관련 : 왜이 C ++ 코드가 Collatz 추측을 테스트하기 위해 손으로 작성한 어셈블리보다 빠른가요? 자세히 설명합니다.)

어쨌든 10 씩 복사 + 시프트는 별도의 mov명령이 필요합니다 . 최근 많은 CPU에서 지연 시간이 0 일 수 있지만 여전히 프런트 엔드 대역폭과 코드 크기가 필요합니다. ( x86의 MOV가 정말 "무료"일 수 있습니까? 왜 이것을 전혀 재현 할 수 없습니까? )

또한 관련이 있습니다. x86에서 2 개의 연속적인 leal 명령어 만 사용하여 레지스터에 37을 곱하는 방법은 무엇입니까? .


컴파일러는 또한 주변 코드를 자유롭게 변환 할 수 있으므로 실제 이동이 발생하지 않거나 다른 작업과 결합됩니다 .

예를 들어 if(x<<1) { }를 사용하여 and상위 비트를 제외한 모든 비트를 확인할 수 있습니다 . x86에서는 . 대신 / test와 같은 명령어를 사용합니다 . 이 최적화는 모든 시프트 수에 대해 작동하며, 많은 수의 시프트가 느리거나 (예 : Pentium 4) 존재하지 않는 (일부 마이크로 컨트롤러) 머신에서도 작동합니다.test eax, 0x7fffffffjz .falseshl eax,1 / jz

많은 ISA에는 단순한 이동 이상의 비트 조작 명령이 있습니다. 예를 들어 PowerPC에는 많은 비트 필드 추출 / 삽입 명령이 있습니다. 또는 ARM에는 다른 명령어의 일부로 소스 피연산자의 시프트가 있습니다. (따라서 이동 / 회전 명령은 move이동 된 소스를 사용하는 의 특수한 형태 일뿐 입니다.)

기억 C 어셈블리 언어가 아닙니다 . 효율적으로 컴파일하기 위해 소스 코드를 조정할 때는 항상 최적화 된 컴파일러 출력을 확인하십시오.

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