비트 연산자를 사용하여 곱셈과 나눗셈을 수행 할 수 있습니다 (예 :
i*2 = i<<1
i*3 = (i<<1) + i;
i*10 = (i<<3) + (i<<1)
등등.
실제로 직접 (i<<3)+(i<<1)
사용 i*10
하는 것보다 10을 곱하는 것이 사용하는 것이 더 빠릅 니까? 이런 식으로 곱하거나 나눌 수없는 입력이 있습니까?
비트 연산자를 사용하여 곱셈과 나눗셈을 수행 할 수 있습니다 (예 :
i*2 = i<<1
i*3 = (i<<1) + i;
i*10 = (i<<3) + (i<<1)
등등.
실제로 직접 (i<<3)+(i<<1)
사용 i*10
하는 것보다 10을 곱하는 것이 사용하는 것이 더 빠릅 니까? 이런 식으로 곱하거나 나눌 수없는 입력이 있습니까?
답변:
짧은 대답 : 가능성이 없습니다.
긴 대답 : 컴파일러에는 대상 프로세서 아키텍처가 가능한 한 빨리 곱하는 방법을 알고있는 최적화 프로그램이 있습니다. 최선의 방법은 컴파일러에게 의도를 명확하게 알리고 (즉, i << 1 대신 i * 2) 가장 빠른 어셈블리 / 머신 코드 시퀀스를 결정하도록하는 것입니다. 프로세서 자체가 곱셈 명령을 일련의 시프트 및 추가 마이크로 코드로 구현했을 수도 있습니다.
결론은 이것에 대해 걱정하는 데 많은 시간을 소비하지 마십시오. 이동하려는 경우 이동하십시오. 곱하기로한다면 곱하십시오. 의미 상 가장 명확한 것을 수행하십시오. 동료가 나중에 감사 할 것입니다. 아니면 다른 방법으로 나중에 저주 할 가능성이 높습니다.
gcc -O3
return i*10
millis() >> 2
. 그냥 나누기를 요청하는 것이 너무 많았 을까요?
i / 32
vs i >> 5
및 i / 4
vs i >> 2
를 테스트 했으며 결과 어셈블리는 정확히 동일했습니다. 나는 나누기를 먼저 사용하는 것을 좋아하지 않았지만 내 의도를 설명하고 출력은 동일합니다.
몇 년 전, 해싱 알고리즘의 두 가지 버전을 벤치마킹했습니다.
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
입니다. 오늘날 분명히 반복자를 사용 하고 있습니다.)
여기에있는 다른 모든 좋은 대답 외에도 나누거나 곱할 때 시프트를 사용하지 않는 또 다른 이유를 지적하겠습니다. 나는 한번에 누군가가 곱셈과 덧셈의 상대적 우선 순위를 잊어 버림으로써 버그를 소개하는 것을 본 적이 없다. 유지 보수 프로그래머가 시프트를 통한 "곱하기"는 논리적 으로 곱셈이지만 구문 적 으로 곱셈과 같은 우선 순위가 아니라는 것을 잊었을 때 버그가 발생하는 것을 보았습니다 . x * 2 + z
와 x << 1 + z
매우 다르다!
숫자 로 작업하는 경우 와 같은 산술 연산자를 사용하십시오 + - * / %
. 비트 배열을 작업하는 경우 비트 트위들 링 연산자를 사용하십시오 & ^ | >>
. 그것들을 섞지 마십시오. 비트 twiddling과 산술이 모두있는 표현식은 발생하는 버그입니다.
프로세서와 컴파일러에 따라 다릅니다. 일부 컴파일러는 이미 이런 식으로 코드를 최적화하지만 다른 컴파일러는 그렇지 않습니다. 따라서이 방법으로 코드를 최적화해야 할 때마다 확인해야합니다.
필사적으로 최적화 해야하는 경우가 아니라면 어셈블리 명령이나 프로세서 사이클을 저장하기 위해 소스 코드를 스크램블하지는 않습니다.
>>
연산자가 빠르며 /
, 부호있는 값이 음수 일 수있는 경우에는 의미 적으로도 우수합니다. x>>4
생성 할 가치가 필요한 경우 보다 훨씬 명확 x < 0 ? -((-1-x)/16)-1 : x/16;
하며 컴파일러가 후자의 표현을 멋진 것으로 최적화하는 방법을 상상할 수 없습니다.
실제로 i * 10을 직접 사용하는 것보다 (i << 3) + (i << 1)을 사용하여 10을 곱하는 것이 더 빠릅니까?
컴퓨터에있을 수도 있고 없을 수도 있습니다. 관심이 있다면 실제 사용량을 측정하십시오.
벤치마킹은 의미있는 작업을하기가 매우 어렵지만 몇 가지 사실을 볼 수 있습니다. 에서 http://www.penguin.cz/~literakl/intel/s.html#SAL 및 http://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
그것은 직원의 나이를 저장하고 있습니다, 그것은 결코 부정적 일 수 없습니다." 특별한 통찰력 >>
이 있다면, 코드에서 명시 적으로 수행하지 않는 한 안전한 최적화가 컴파일러에 의해 전달 될 수 있습니다. 그러나 이런 종류의 통찰력을 얻지 못할 때까지는 위험 하고 거의 유용하지 않으며 같은 코드로 작업하는 다른 프로그래머는 데이터에 대한 비정상적인 기대에 대해 집에 내기를 걸 었음을 알지 못합니다. 처리 할 것입니다. "최적화"로 인해 그들에게 완전히 안전한 변화가 역효과를 낳을 수 있습니다.
이런 식으로 곱하거나 나눌 수없는 입력이 있습니까?
예 ... 위에서 언급했듯이 음수는 비트 시프 팅에 의해 "분할"될 때 구현 정의 동작을 갖습니다.
intVal>>1
과 다른 의미론을 갖습니다 intVal/2
. 이식 가능한 방식으로 평범한 아키텍처가 얻을 수있는 가치를 계산해야하는 경우 intVal >> 1
, 표현은 좀 더 복잡하고 읽기 어려워 야하며, 실제보다 열등한 코드를 생성 할 수 있습니다 intVal >> 1
.
방금 내 컴퓨터에서 이것을 컴파일하려고 시도했습니다.
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
이 버전은 손쉬운 이동 및 추가 기능을 통해 수동으로 최적화 된 코드보다 빠릅니다.
당신은 정말 더 나은에 그래서 단순히 쓰기, 컴파일러 가지고 올 것입니다 무엇인지 결코 정상 곱셈을하고 어디 그에게 그가 매우 정확한 경우를 제외하고,하고자하는 방법을 최적화 할 수 알고 최적화 할 수없는 컴파일러.
vector<T>::size()
. 내 컴파일러는 아주 고대였습니다! :)
쉬프팅은 일반적으로 명령어 레벨에서 곱하는 것보다 훨씬 빠르지 만 조기 최적화를하는 데 시간을 낭비하고있을 수 있습니다. 컴파일러는 컴파일 타임에 이러한 최적화를 수행 할 수 있습니다. 직접 작성하면 가독성에 영향을 미치며 성능에는 영향을 미치지 않습니다. 프로필을 작성하고 병목 현상을 발견 한 경우 이와 같은 작업을 수행하는 것이 좋습니다.
실제로 '매직 디비전'으로 알려진 디비전 트릭은 실제로 엄청난 성과를 낼 수 있습니다. 다시 필요한지 확인하려면 먼저 프로파일 링해야합니다. 그러나 그것을 사용하면 동일한 부서 의미에 필요한 명령을 파악하는 데 도움이되는 유용한 프로그램이 있습니다. 예를 들면 다음과 같습니다. 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
시프트 및 정수 곱셈 명령어는 대부분의 최신 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
따라서 컴파일러를 돕고 싶다면 피제수의 변수 또는 표현식이 명시 적으로 부호가 없는지 확인하십시오.
절대적으로 필요한 경우가 아니라면 코드 의도에 곱셈 / 나눗셈이 아닌 이동이 필요하지 않으면하지 마십시오.
일반적으로 적은 수의 기계 사이클을 절약 할 수 있습니다 (또는 컴파일러가 최적화 할 항목을 더 잘 알고 있기 때문에 느슨 함). 그러나 비용은 가치가 없습니다. 실제 작업보다는 사소한 세부 사항에 시간을 소비하여 코드를 유지하는 것이 더 어려워지고 당신의 동료들은 당신을 저주 할 것입니다.
각각의 저장된주기는 몇 분의 런타임을 의미하는 고부하 계산을 위해 수행해야 할 수도 있습니다. 그러나 한 번에 한 곳을 최적화하고 매번 성능 테스트를 수행하여 실제로 더 빠른지 또는 컴파일러 논리를 위반했는지 확인해야합니다.
내가 일부 기계에서 아는 한 곱셈에는 최대 16 ~ 32 기계 사이클이 필요할 수 있습니다. 그래서 예 , 기계의 종류에 따라, bitshift 연산자는 빠른 곱하기 / 나누기보다.
그러나 특정 기계에는 곱셈 / 나눗셈에 대한 특수 명령어가 포함 된 수학 프로세서가 있습니다.
Drew Hall의 답변에 동의합니다. 대답은 몇 가지 추가 메모를 사용할 수 있습니다.
대다수의 소프트웨어 개발자에게 프로세서와 컴파일러는 더 이상 문제와 관련이 없습니다. 우리 대부분은 8088과 MS-DOS를 훨씬 능가합니다. 임베디드 프로세서를 위해 여전히 개발중인 사람들에게만 관련이 있습니다 ...
내 소프트웨어 회사에서는 모든 수학에 수학 (add / sub / mul / div)을 사용해야합니다. 데이터 형식간에 변환 할 때는 Shift를 사용해야합니다 (예 : n >> 256이 아닌 n >> 8로 바이트로 줄임 .
부호있는 정수와 오른쪽 시프트 대 나누기의 경우 차이를 만들 수 있습니다. 음수의 경우, 이동은 음의 무한대로 반올림되고 나눗셈은 0을 향해 반올림됩니다. 물론 컴파일러는 나눗셈을 더 저렴한 것으로 변경하지만 일반적으로 나누기와 같은 반올림 동작을 갖는 것으로 변경합니다. 변수가 음수가 아니거나 단순히 그렇지 않다는 것을 증명할 수 없기 때문입니다. 케어. 따라서 숫자가 음수가 아니라는 것을 증명할 수 있거나 어떤 방식으로 반올림하지 않든 상관없이 차이를 만들 가능성이 높은 방식으로 최적화를 수행 할 수 있습니다.
unsigned
같은 난수에 대해 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의 제곱이 아닌 경우 상당한 속도 저하가있을 수 있습니다.
이 #도 프로세서, 컴파일러 (또는 인터프리터-간결함을 위해 파이썬에서 한 것)에 따라 변경됩니다.
다른 모든 사람들과 마찬가지로 조기 최적화하지 마십시오. 읽을 수있는 코드를 작성하고, 빠르지 않은 경우 프로파일을 작성한 다음 느린 부분을 최적화하십시오. 컴파일러는 최적화하는 것보다 훨씬 좋습니다.
컴파일러는 입력 집합이 축소 된 경우에만 작동하기 때문에 컴파일러가 수행 할 수없는 최적화가 있습니다.
아래에는 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;
}
한 경우에는 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의 곱셈 / 나눗셈 만 수행하기를 원하지 않기 때문입니다.
실제로 더 빠른지 여부는 실제로 사용 된 하드웨어와 컴파일러에 따라 다릅니다 .
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 를 입력 한 내용과 독립적으로 자신의 최상의 솔루션을 결정하기에 충분히 똑똑 하다고 생각할 수 있습니다 .
나도 집을 이길 수 있는지 알고 싶었다. 이것은 숫자의 곱셈에 의한 모든 숫자에 대한 비트 단위입니다. 내가 만든 매크로는 일반 곱셈보다 약 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;
}