64 비트 DIV 명령어가 2로 나눌 수있는 좋은 방법이라고 생각한다면 컴파일러의 asm 출력이 -O0
(빠른 컴파일, 추가 최적화 및 메모리 저장 / 재로드 후) 디버거가 변수를 수정할 수 있도록 모든 C 문 앞에).
효율적인 asm 작성 방법 은 Agner Fog의 최적화 어셈블리 안내서 를 참조하십시오 . 또한 특정 CPU에 대한 특정 세부 사항에 대한 지시 사항 테이블과 마이크로 아치 안내서가 있습니다. 참조x86 더 많은 perf 링크를위한 wiki 태그.
손으로 작성한 asm을 사용하여 컴파일러를 두드리는 방법에 대한 일반적인 질문도 참조하십시오. 인라인 어셈블리 언어가 네이티브 C ++ 코드보다 느립니까? . TL : DR : 그렇습니다 (예 :이 질문처럼).
일반적으로 컴파일러가 효율적으로 컴파일 할 수있는 C ++를 작성하려고 하면 컴파일러가 그 일을하도록하는 것이 좋습니다 . 또한 컴파일 된 언어보다 어셈블리가 더 빠릅니까? . 답변 중 하나는 다양한 C 컴파일러가 멋진 트릭으로 일부 간단한 함수를 최적화하는 방법을 보여주는 깔끔한 슬라이드로 연결됩니다 . Matt Godbolt의 CppCon2017 talk“ 최근에 컴파일러에서 수행 한 작업은 무엇입니까? 컴파일러의 뚜껑을 풀다 ”는 비슷한 맥락에있다.
even:
mov rbx, 2
xor rdx, rdx
div rbx
Intel Haswell에서는 대기 시간이 32-96 사이클 이고 처리량은 21-74 사이클 당 div r64
36 개의 uop 입니다. (또한 RBX를 설정하고 RDX를 0으로 설정하는 2 개의 UOP가 있지만 비 순차적 실행은 조기에 실행할 수 있습니다). DIV와 같은 많은 수의 명령은 마이크로 코딩되며, 이로 인해 프런트 엔드 병목 현상이 발생할 수 있습니다. 이 경우 대기 시간이 루프 관련 종속성 체인의 일부이기 때문에 가장 중요한 요소입니다.
shr rax, 1
동일한 부호없는 구분을 수행합니다 .1c 대기 시간으로 1uop이며 클럭 사이클 당 2 개를 실행할 수 있습니다.
비교하자면 32 비트 분할은 빠르지 만 여전히 끔찍한 대 시프트입니다. idiv r32
Haswell에서 9 개의 Uops, 22-29c의 대기 시간 및 8-11c의 처리량 당 하나입니다.
gcc의 -O0
asm 출력 ( Godbolt 컴파일러 탐색기 ) 을 보면 알 수 있듯이 shifts 명령어 만 사용합니다 . clang -O0
은 64 비트 IDIV를 두 번 사용하더라도 생각한대로 순진하게 컴파일됩니다. (최적화 할 때 컴파일러는 소스가 분할을 할 때 IDIV의 두 출력을 사용하고 IDIV를 전혀 사용하지 않으면 동일한 피연산자로 모듈러스를 사용합니다)
GCC에는 완전히 순진한 모드가 없습니다. 항상 GIMPLE을 통해 변환되므로 일부 "최적화"를 비활성화 할 수 없습니다 . 여기에는 상수로 구분하고 IDIV를 피하기 위해 시프트 (2의 거듭 제곱) 또는 고정 소수점 곱하기 역 (2의 비 거듭 제곱)을 사용 div_by_13
하는 것이 포함됩니다 (위의 godbolt 링크 참조).
gcc -Os
불행히도 곱하기 역 코드가 약간 더 크지 만 훨씬 더 빠른 경우에도 (크기 최적화) 는 비-제곱 나누기에 IDIV를 사용합니다.
컴파일러 돕기
(이 경우 요약 : use uint64_t n
)
우선, 최적화 된 컴파일러 출력을 보는 것만 흥미 롭습니다. ( -O3
). -O0
속도는 기본적으로 의미가 없습니다.
asm 출력을보십시오 (Godbolt에서 또는 GCC / clang 어셈블리 출력에서 "노이즈"를 제거하는 방법? 참조 ). 컴파일러가 처음에 최적의 코드를 만들지 않는 경우 : 컴파일러가 더 나은 코드를 작성하도록 안내하는 방식으로 C / C ++ 소스를 작성하는 것이 일반적으로 가장 좋은 방법 입니다. asm과 효율적인 것이 무엇인지 알아야하지만이 지식을 간접적으로 적용해야합니다. 컴파일러는 좋은 아이디어의 원천이기도합니다. 때로는 clang이 멋진 작업을 수행하고 gcc를 똑같이 수행 할 수 있습니다. 이 답변 과 아래 @Veedrac의 코드에서 언 롤링되지 않은 루프 로 수행 한 작업을 참조하십시오 .)
이 접근 방식은 이식성이 뛰어나 20 년 후에는 미래의 하드웨어 (x86에 관계없이)에서 새로운 ISA 확장 또는 자동 벡터화를 사용하여 효율적으로 컴파일 할 수 있습니다. 15 년 전의 손으로 쓴 x86-64 asm은 일반적으로 Skylake에 맞게 최적으로 조정되지 않았습니다. 예를 들어 compare & branch macro-fusion은 당시에는 없었습니다. 하나의 마이크로 아키텍처에 대한 수제 asm에 대해 현재 최적 인 것은 다른 현재 및 미래의 CPU에 적합하지 않을 수 있습니다. @johnfound의 답변에 대한 의견은 이 코드에 큰 영향을 미치는 AMD Bulldozer와 Intel Haswell의 주요 차이점에 대해 설명합니다. 그러나 이론적으로, g++ -O3 -march=bdver3
그리고 g++ -O3 -march=skylake
옳은 일을 할 것입니다. (또는 -march=native
.) 또는 -mtune=...
다른 CPU가 지원하지 않는 명령을 사용하지 않고 그냥 조정하십시오.
내 생각에 당신이 관심있는 현재 CPU에 좋은 컴파일러를 asm으로 인도하는 것은 미래의 컴파일러에게는 문제가되지 않아야합니다. 코드를 변환하는 방법을 찾는 데 현재의 컴파일러보다 더 나아졌으며 향후 CPU에 적합한 방법을 찾을 수 있습니다. 그럼에도 불구하고, 미래 x86은 현재 x86에서 좋은 점에서 끔찍하지 않을 것입니다. 향후 컴파일러는 무언가 더 잘 보이지 않으면 C 소스의 데이터 이동과 같은 것을 구현하는 동안 asm 관련 함정을 피할 것입니다.
손으로 쓴 asm은 옵티마이 저의 블랙 박스이므로 인라인이 입력을 컴파일 타임 상수로 만들면 상수 전파가 작동하지 않습니다. 다른 최적화도 영향을받습니다. asm을 사용하기 전에 https://gcc.gnu.org/wiki/DontUseInlineAsm 을 읽으십시오 . (그리고 MSVC 스타일 인라인 asm을 피하십시오 : 입출력은 오버 헤드를 추가하는 메모리를 거쳐야 합니다 .)
이 경우 : n
서명 된 유형이 있으며 gcc는 올바른 반올림을 제공하는 SAR / SHR / ADD 시퀀스를 사용합니다. (음수 입력의 경우 IDIV 및 산술 시프트 "라운드"가 다릅니다. SAR insn set ref manual entry 참조 ). (gcc n
가 부정적이거나 무엇 을 증명할 수 없었지만 IDK가 실패한 경우 IDK . 서명 된 오버플로는 정의되지 않은 동작이므로 가능했을 것입니다.)
을 사용해야 uint64_t n
하므로 SHR 만 가능합니다. 따라서 long
32 비트 (예 : x86-64 Windows) 인 시스템에 이식 가능합니다 .
BTW, gcc의 최적화 된 asm 출력은 (을 사용하여 unsigned long n
) 꽤 좋아 보입니다 : 인라인 내부 루프 main()
가 다음 을 수행합니다.
# from gcc5.4 -O3 plus my comments
# edx= count=1
# rax= uint64_t n
.L9: # do{
lea rcx, [rax+1+rax*2] # rcx = 3*n + 1
mov rdi, rax
shr rdi # rdi = n>>1;
test al, 1 # set flags based on n%2 (aka n&1)
mov rax, rcx
cmove rax, rdi # n= (n%2) ? 3*n+1 : n/2;
add edx, 1 # ++count;
cmp rax, 1
jne .L9 #}while(n!=1)
cmp/branch to update max and maxi, and then do the next n
내부 루프는 분기가 없으며 루프 수행 종속성 체인의 중요한 경로는 다음과 같습니다.
- 3 성분 LEA (3 사이클)
- cmov (Haswell에서 2 사이클, Broadwell에서 1c 이상).
총 : 반복 당 5주기, 대기 시간 병목 현상 . 비 순차적 실행은 이것과 병행하여 다른 모든 것을 처리합니다.
FLAGS 입력 cmov
(TEST에서 생산)은 RAX 입력 (LEA-> MOV에서)보다 생성 속도가 빠르므로 중요한 경로에 있지 않습니다.
마찬가지로 CMOV의 RDI 입력을 생성하는 MOV-> SHR도 LEA보다 빠르기 때문에 중요한 경로를 벗어납니다. IvyBridge 이상의 MOV는 대기 시간이 없습니다 (레지스터 이름 변경시 처리됨). (여전히 파이프 라인의 슬롯과 슬롯이 필요하므로 여유 시간이 없으며 대기 시간이 없습니다.) LEA dep 체인의 추가 MOV는 다른 CPU의 병목 현상의 일부입니다.
cmp / jne도 임계 경로의 일부가 아닙니다. 제어 경로는 임계 경로에 대한 데이터 종속성과 달리 분기 예측 + 추측 실행으로 처리되므로 루프로 전달되지 않습니다.
컴파일러 구타
GCC는 여기서 꽤 잘했습니다. P4와 부분 플래그 수정 명령에 대한 거짓 의존성을 신경 쓰지 않기 때문에 inc edx
대신을add edx, 1
사용하여 하나의 코드 바이트를 절약 할 수 있습니다.
또한 모든 MOV 명령어를 저장할 수 있으며 TEST : SHR은 CF = 비트 시프트 된 비트를 설정하므로 / cmovc
대신 사용할 수 있습니다 .test
cmovz
### Hand-optimized version of what gcc does
.L9: #do{
lea rcx, [rax+1+rax*2] # rcx = 3*n + 1
shr rax, 1 # n>>=1; CF = n&1 = n%2
cmovc rax, rcx # n= (n&1) ? 3*n+1 : n/2;
inc edx # ++count;
cmp rax, 1
jne .L9 #}while(n!=1)
또 다른 영리한 트릭에 대한 @ johnfound의 답변을 참조하십시오 : SHR의 플래그 결과를 분기하여 CMMP를 사용하여 CMP를 제거하십시오 .n은 1 (또는 0)로 시작하는 경우에만 0입니다. (재미있는 사실 : Nehalem 또는 그 이전 버전에서 카운트가 1 = 1 인 SHR은 플래그 결과를 읽으면 실속을 일으 킵니다 . 그런 식으로 단일 우팝 방식으로 만들었습니다 .shift-by-1 특수 인코딩은 괜찮습니다.)
MOV를 피하면 Haswell에서 대기 시간이 전혀 도움이되지 않습니다 ( x86의 MOV가 실제로 "무료"일 수 있습니까? 왜 이것을 전혀 재현 할 수 없습니까? ). MOV가 지연 시간이 0이 아닌 Intel pre-IvB 및 AMD Bulldozer 제품군과 같은 CPU에서 크게 도움 이됩니다. 컴파일러의 낭비 된 MOV 명령어는 중요한 경로에 영향을줍니다. BD의 복합 LEA 및 CMOV는 대기 시간이 각각 짧고 (각각 2c와 1c), 대기 시간의 더 큰 부분입니다. 또한 처리량 병목 현상에는 두 개의 정수 ALU 파이프 만 있기 때문에 문제가됩니다. @ johnfound 's answer 에서 AMD CPU의 타이밍 결과를 확인할 수 있습니다.
Haswell에서도이 버전은 중요하지 않은 uop이 중요 경로의 포트에서 실행 포트를 훔쳐 실행주기를 1주기 지연시키는 경우에 따라 약간의 지연을 피함으로써 조금 도움이 될 수 있습니다. 이를 자원 충돌이라고합니다. 또한 n
인터리브 루프에서 여러 값을 병렬로 수행 할 때 도움이되는 레지스터를 저장합니다 (아래 참조).
LEA의 대기 시간은 주소 지정 모드 , Intel SnB 제품군 CPU에 따라 다릅니다 . 3 개의 구성 요소에 대해 3c ( [base+idx+const]
, 두 개의 개별 추가가 필요함), 구성 요소가 2 개 이하인 1c 만 (하나의 추가). 일부 CPU (예 : Core2)는 단일 주기로 3 개 구성 요소 LEA를 수행하지만 SnB 제품군은 그렇지 않습니다. 더 나쁜, 더 2C의 마이크로 연산이 없기 때문에 인텔 SNB-가족은 대기 시간을 표준화 , 그렇지 않으면 3 요소 레아 불도저 같은 단지 2C 될 것이다. (3- 컴포넌트 LEA는 AMD에서도 느리게 진행됩니다).
따라서 lea rcx, [rax + rax*2]
/ inc rcx
는 lea rcx, [rax + rax*2 + 1]
Haswell과 같은 Intel SnB 제품군 CPU에서 2c의 대기 시간으로,보다 빠릅니다 . BD에서는 손익 분기점이되고 Core2에서는 더 나빠집니다. 일반적으로 1c 대기 시간을 절약 할 가치가없는 추가 uop가 필요하지만 대기 시간이 여기에 큰 병목 현상이 있으며 Haswell은 추가 uop 처리량을 처리 할 수있는 충분한 파이프 라인을 가지고 있습니다.
gcc, icc 또는 clang (godbolt)은 항상 AND 또는 TEST를 사용하여 SHR의 CF 출력을 사용하지 않았습니다 . 바보 컴파일러. : P 그들은 복잡한 기계의 훌륭한 조각이지만, 영리한 인간은 종종 소규모 문제로 그들을 이길 수 있습니다. (물론 그것에 대해 생각하는 데 수천에서 수백만 시간이 더 걸립니다!) 컴파일러는 모든 가능한 모든 방법을 검색하기 위해 철저한 알고리즘을 사용하지 않습니다. 많은 인라인 코드를 최적화 할 때 시간이 너무 오래 걸리기 때문입니다. 또한 목표 마이크로 아키텍처에서 파이프 라인을 모델링하지 않으며 적어도 IACA 또는 기타 정적 분석 도구 와 동일하게 자세히 설명하지는 않으며 일부 휴리스틱 만 사용합니다.)
간단한 루프 언 롤링은 도움이되지 않습니다 . 이 루프 병목 현상은 루프 오버 헤드 / 처리량이 아니라 루프 수행 종속성 체인의 대기 시간에 병목 현상이 발생합니다. 이는 CPU가 두 개의 스레드에서 명령을 인터리브하는 데 많은 시간이 있기 때문에 하이퍼 스레딩 (또는 다른 종류의 SMT)과 잘 작동한다는 것을 의미합니다. 이것은 루프를 병렬화한다는 것을 의미 main
하지만 각 스레드는 n
값 범위를 확인 하고 결과적으로 정수 쌍을 생성 할 수 있기 때문에 좋습니다.
단일 스레드 내에서 손으로 인터리빙하는 것도 가능할 수 있습니다 . 각각 한 쌍의 레지스터 만 취하므로 모두 같은 max
/를 업데이트 할 수 있기 때문에 한 쌍의 숫자에 대한 시퀀스를 병렬로 계산할 수 있습니다 maxi
. 이것은 더 많은 명령어 수준의 병렬 처리를 만듭니다 .
트릭은 다른 시작 값 쌍을 얻기 전에 모든 n
값에 도달 할 때까지 기다릴 것인지 아니면 다른 시퀀스의 레지스터를 건드리지 않고 종료 조건에 도달 한 단 하나의 새로운 시작점을 나눌 수 있는지 여부를 결정하는 것입니다. 아마도 각 체인이 유용한 데이터를 처리하도록하는 것이 가장 좋습니다. 그렇지 않으면 조건부로 카운터를 증가시켜야합니다.1
n
아직 n
도달 1
하지 않은 벡터 요소의 카운터를 조건부로 증가시키기 위해 SSE 압축 비교 항목을 사용하여이 작업을 수행 할 수도 있습니다. 그런 다음 SIMD 조건부 증가 구현의 훨씬 더 긴 대기 시간을 숨기려면 더 많은 n
값의 벡터를 대기 에 유지해야합니다 . 256b 벡터 (4x uint64_t
) 로만 가치가 있습니다.
1
"sticky"를 감지하는 가장 좋은 전략 은 카운터를 증가시키기 위해 추가 한 모든 벡터를 마스킹하는 것입니다. 따라서 1
요소에서를 본 후에 증가 벡터는 0이되고 + = 0은 작동하지 않습니다.
수동 벡터화를위한 검증되지 않은 아이디어
# starting with YMM0 = [ n_d, n_c, n_b, n_a ] (64-bit elements)
# ymm4 = _mm256_set1_epi64x(1): increment vector
# ymm5 = all-zeros: count vector
.inner_loop:
vpaddq ymm1, ymm0, xmm0
vpaddq ymm1, ymm1, xmm0
vpaddq ymm1, ymm1, set1_epi64(1) # ymm1= 3*n + 1. Maybe could do this more efficiently?
vprllq ymm3, ymm0, 63 # shift bit 1 to the sign bit
vpsrlq ymm0, ymm0, 1 # n /= 2
# FP blend between integer insns may cost extra bypass latency, but integer blends don't have 1 bit controlling a whole qword.
vpblendvpd ymm0, ymm0, ymm1, ymm3 # variable blend controlled by the sign bit of each 64-bit element. I might have the source operands backwards, I always have to look this up.
# ymm0 = updated n in each element.
vpcmpeqq ymm1, ymm0, set1_epi64(1)
vpandn ymm4, ymm1, ymm4 # zero out elements of ymm4 where the compare was true
vpaddq ymm5, ymm5, ymm4 # count++ in elements where n has never been == 1
vptest ymm4, ymm4
jnz .inner_loop
# Fall through when all the n values have reached 1 at some point, and our increment vector is all-zero
vextracti128 ymm0, ymm5, 1
vpmaxq .... crap this doesn't exist
# Actually just delay doing a horizontal max until the very very end. But you need some way to record max and maxi.
직접 작성한 asm 대신 내장 함수를 사용하여이를 구현할 수 있으며 구현해야합니다.
알고리즘 / 구현 개선 :
보다 효율적인 asm으로 동일한 로직을 구현하는 것 외에도 로직을 단순화하거나 중복 작업을 피하는 방법을 찾으십시오. 예를 들어 시퀀스의 공통 엔딩을 감지하기 위해 메모합니다. 또는 더 나은 방법은 한 번에 8 개의 후행 비트를 보는 것입니다.
@EOF는 tzcnt
(또는 bsf
)를 사용하여 한 번에 여러 n/=2
반복 을 수행 할 수 있다고 지적합니다 . 아마도 SIMD 벡터화보다 낫습니다. SSE 또는 AVX 명령으로는 그렇게 할 수 없습니다. 그래도 n
다른 정수 레지스터에서 여러 스칼라 를 병렬 로 수행하는 것과 여전히 호환됩니다 .
따라서 루프는 다음과 같습니다.
goto loop_entry; // C++ structured like the asm, for illustration only
do {
n = n*3 + 1;
loop_entry:
shift = _tzcnt_u64(n);
n >>= shift;
count += shift;
} while(n != 1);
이는 반복 횟수를 크게 줄일 수 있지만 BMI2가없는 Intel SnB 제품군 CPU에서는 가변 카운트 이동이 느립니다. 3 개의 UOP, 2C 레이턴시. (카운트 = 0은 플래그가 수정되지 않았 음을 의미하기 때문에 FLAGS에 대한 입력 종속성이 있습니다. 데이터 종속성으로이를 처리하고, UOP는 2 개의 입력 만 가질 수 있기 때문에 여러 UOP를 가져옵니다 (HSW / BDW 사전)). 이것은 x86의 미친 CISC 디자인에 대해 불평하는 사람들이 말하는 종류입니다. ISA가 오늘날 처음부터 거의 비슷한 방식으로 설계되었을 때보 다 x86 CPU가 느려집니다. (즉, 이것은 속도 / 전력을 소비하는 "x86 세금"의 일부입니다.) SHRX / SHLX / SARX (BMI2)는 큰 승리입니다 (1 uop / 1c 대기 시간).
또한 tzcnt (Haswell 이상에서 3c)를 임계 경로에 배치하므로 루프로 전달되는 종속성 체인의 총 대기 시간이 크게 길어집니다. 그러나 CMOV가 필요하거나 레지스터 유지를 준비 할 필요가 없습니다 n>>1
. @Veedrac의 대답은 여러 반복에 대해 tzcnt / shift를 지연시켜이 모든 것을 극복합니다. 이는 매우 효과적입니다 (아래 참조).
BSF 또는 TZCNT를 상호 호환 적으로 안전하게 사용할 n
수 있습니다. 그 시점에서는 절대 0이 될 수 없기 때문 입니다. TZCNT의 기계 코드는 BMI1을 지원하지 않는 CPU에서 BSF로 디코딩합니다. 의미없는 접두사는 무시되므로 REP BSF는 BSF로 실행됩니다.
TZCNT는이를 지원하는 AMD CPU에서 BSF보다 성능이 훨씬 뛰어나므로 REP BSF
출력이 아니라 입력이 0 인 경우 ZF 설정에 신경 쓰지 않아도을 사용하는 것이 좋습니다 . __builtin_ctzll
와 함께 사용할 때 일부 컴파일러에서이 작업을 수행합니다 -mno-bmi
.
인텔 CPU에서도 동일하게 수행되므로 중요한 경우 바이트를 저장하십시오. Intel의 TZCNT (Skylake 이전)는 입력 = 0 인 BSF가 대상을 수정하지 않은 상태로 유지하는 문서화되지 않은 동작을 지원하기 위해 BSF와 마찬가지로 가정 된 쓰기 전용 출력 피연산자에 대해 여전히 잘못된 종속성을 가지고 있습니다. 따라서 Skylake에만 최적화하지 않는 한이 문제를 해결해야하므로 여분의 REP 바이트에서 얻을 것이 없습니다. 는 x86 ISA 설명서는 말아야 무엇인가에 따라 널리 사용되는 코드를 깨는 방지하기 위해, 필요, 또는 소급 허용되지 않습니다 어떤 (인텔는 종종 위 넘는다. 예를 들어, 윈도우 9x의는 TLB 엔트리에는 투기 프리 페치지지 않습니다 안전했다, 인텔이 TLB 관리 규칙을 업데이트하기 전에 코드 작성 시점 )
어쨌든 Haswell의 LZCNT / TZCNT는 POPCNT와 같은 잘못된 결과를 가지고 있습니다 : 이 Q & A를보십시오 . 이것이 @Veedrac의 코드에 대한 gcc의 asm 출력에서 dst = src를 사용하지 않을 때 TZCNT의 대상으로 사용하려는 레지스터에서 xor-zeroing 으로 dep 체인을 깨는 것을 볼 수 있습니다. TZCNT / LZCNT / POPCNT는 대상을 정의되지 않거나 수정하지 않은 상태로 두지 않으므로 인텔 CPU의 출력에 대한이 잘못된 종속성은 성능 버그 / 제한입니다. 아마도 동일한 실행 장치로 이동하는 다른 UOP처럼 행동하게하는 것은 일부 트랜지스터 / 전력의 가치가 있습니다. 유일한 성능 향상은 또 다른 uarch 제한과의 상호 작용 입니다. 인덱싱 된 주소 지정 모드로 메모리 피연산자를 마이크로 퓨즈 할 수 있습니다 Haswell에서, 그러나 Intel이 LZCNT / TZCNT에 대한 잘못된 뎁을 제거한 Skylake에서는 인덱싱 된 어드레싱 모드를 "라미네이팅"하지 않지만 POPCNT는 여전히 가산기 모드를 마이크로 퓨즈 할 수 있습니다.
다른 답변의 아이디어 / 코드 개선 :
@hidefromkgb의 답변 은 3n + 1 후에 한 번의 올바른 이동을 보장한다는 훌륭한 관찰 결과가 있습니다. 단계 사이의 점검을 생략하는 것보다 훨씬 더 효율적으로 계산할 수 있습니다. 그 대답의 asm 구현은 깨졌지만 (OF에 의존하고, SHRD 후에 카운트가> 1로 정의되지 않음) 느리다 : ROR rdi,2
보다 빠르다.보다 SHRD rdi,rdi,2
중요한 경로에서 두 개의 CMOV 명령어를 사용하는 것이 여분의 테스트보다 느리다. 병렬로 실행할 수 있습니다.
나는 깔끔하고 개선 된 C (컴파일러가 더 나은 asm을 생성하도록 안내)를 넣고 Godbolt에 대해 더 빠른 asm (C 아래 주석에서)을 테스트하고 작동 : @hidefromkgb의 답변 링크를 참조하십시오 . (이 답변은 큰 Godbolt URL에서 30k 자 제한에 도달했지만 goo.gl에 대해서는 짧은 링크가 썩을 수 있고 너무 길었습니다.)
또한 출력 인쇄를 개선하여 문자열로 변환하고 한 write()
번에 한 문자 씩 쓰는 대신 문자열을 만들었 습니다. 이것은 perf stat ./collatz
(성능 카운터를 기록하기 위해) 전체 프로그램의 타이밍에 미치는 영향을 최소화하며 중요하지 않은 일부 asm을 난독 화했습니다.
@Veedrac의 코드
우리가 아는 한 오른쪽 이동에서 약간의 속도 향상을 얻었습니다. 필요로하는 에서 루프를 계속 확인하면서 . Core1Duo (Merom)에서 제한값 1e8의 경우 7.5 초에서 풀림 계수가 16의 7.275로 줄어 듭니다.
Godbolt에 대한 코드 + 주석 . 이 버전을 clang과 함께 사용하지 마십시오. 지연 루프와 함께 어리석은 일을합니다. tmp 카운터를 사용하고 나중에 k
추가하면 count
clang의 기능이 변경되지만 gcc 가 약간 손상됩니다.
의견에서 토론 참조 : Veedrac의 코드는 BMI1 (예 : Celeron / Pentium)이있는 CPU에서 우수합니다 .