GCC 최적화가 활성화된 상태에서 스트렌을 사용하는 코드가 6.5배 느리게 표시되는 이유는?
벤치마킹하고 싶었는데glibc
의strlen
어떤 이유에선지 작동하고 GCC에서 최적화가 가능한 상태에서 성능이 훨씬 느리다는 것을 알게 되었고, 나는 그 이유를 모르겠다.
내 암호는 다음과 같다.
#include <time.h>
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
int main() {
char *s = calloc(1 << 20, 1);
memset(s, 65, 1000000);
clock_t start = clock();
for (int i = 0; i < 128; ++i) {
s[strlen(s)] = 'A';
}
clock_t end = clock();
printf("%lld\n", (long long)(end - start));
return 0;
}
내 기계에서 출력:
$ gcc test.c && ./a.out
13336
$ gcc -O1 test.c && ./a.out
199004
$ gcc -O2 test.c && ./a.out
83415
$ gcc -O3 test.c && ./a.out
83415
어떻게 해서든 최적화를 가능하게 하면 그것이 더 오래 실행되게 된다.
Godbolt의 컴파일러 탐색기에서 코드를 테스트하면 다음과 같은 설명이 제공된다.
- 에서
-O0
또는 최적화 없이 생성된 코드는 C 라이브러리 함수를 호출한다.strlen
; - 에서
-O1
생성된 코드는 다음을 사용하는 간단한 인라인 확장을 사용한다.rep scasb
지시 - 에서
-O2
그리고 위에서 생성된 코드는 보다 정교한 인라인 확장을 사용한다.
코드를 반복적으로 벤치마킹하면 한 번의 실행에서 다른 실행으로 상당한 차이가 나타나지만, 반복 횟수를 늘리면 다음과 같은 결과가 나타난다.
- 그
-O1
코드가 C 라이브러리 구현보다 훨씬 느림:32240
대3090
- 그
-O2
코드는 보다 빠르다.-O1
C 그래그나나 여전히 C ibrary보다 더 느리다8570
대3090
.
이 행동은 에 특유하다.gcc
그리고 GNU libc.OS/X에서 동일한 테스트 수행clang
그리고 애플의 Libc는 큰 차이를 보이지 않는데, 이는 Godbolt가 보여주는 것처럼 놀라운 일이 아니다.clang
C 라이브러리에 대한 호출을 생성함strlen
모든 최적화 수준에서
이는 gcc/glibc에서 버그로 간주될 수 있지만, 보다 광범위한 벤치마킹은 통화의 오버헤드를 보여줄 수 있다.strlen
작은 문자열에 대한 인라인 코드의 성능 부족보다 더 중요한 영향을 미친다.벤치마크의 문자열은 드물게 크기 때문에 매우 긴 문자열에만 벤치마크를 집중하면 의미 있는 결과를 얻을 수 없을 수 있다.
나는 이 벤치마크를 개선하고 다양한 끈 길이를 테스트했다.인라인 코드가 생성된 3.10GHz @ 3.10GHz 인텔(R) 코어(TM) i3-2100 CPU에서 실행되는 gcc(Debian 4.7.2-5) 4.7.2 Linux 벤치마크에서 나타난다.-O1
적당히 긴 문자열의 경우 10배만큼 항상 느리지만-O2
libcb보다 뿐이다.strlen
아주 짧은 현악기용이고 긴 현악기용 절반의 빠른 현악기용이다.이 데이터로부터, GNU C 라이브러리 버전의strlen
적어도 내 특정 하드웨어에서 대부분의 문자열 길이에 대해서는 상당히 효율적이다.또한 캐슁은 벤치마크 측정에 큰 영향을 미친다는 점을 유념하십시오.
업데이트된 코드:
#include <stdlib.h>
#include <string.h>
#include <time.h>
void benchmark(int repeat, int minlen, int maxlen) {
char *s = malloc(maxlen + 1);
memset(s, 'A', minlen);
long long bytes = 0, calls = 0;
clock_t clk = clock();
for (int n = 0; n < repeat; n++) {
for (int i = minlen; i < maxlen; ++i) {
bytes += i + 1;
calls += 1;
s[i] = '\0';
s[strlen(s)] = 'A';
}
}
clk = clock() - clk;
free(s);
double avglen = (minlen + maxlen - 1) / 2.0;
double ns = (double)clk * 1e9 / CLOCKS_PER_SEC;
printf("average length %7.0f -> avg time: %7.3f ns/byte, %7.3f ns/call\n",
avglen, ns / bytes, ns / calls);
}
int main() {
benchmark(10000000, 0, 1);
benchmark(1000000, 0, 10);
benchmark(1000000, 5, 15);
benchmark(100000, 0, 100);
benchmark(100000, 50, 150);
benchmark(10000, 0, 1000);
benchmark(10000, 500, 1500);
benchmark(1000, 0, 10000);
benchmark(1000, 5000, 15000);
benchmark(100, 1000000 - 50, 1000000 + 50);
return 0;
}
출력 내용은 다음과 같다.
chqrlie> gcc -std=c99 -O0 benchstrlen.c &&./a.out평균 길이 0 -> 평균 시간: 14.000ns/byte, 14.000ns/call평균 길이 4 -> 평균 시간: 2.364ns/byte, 13.000ns/call평균 길이 10 -> 평균 시간: 1.238ns/byte, 13.000ns/call평균 길이 50 -> 평균 시간: 0.317ns/byte, 16.000ns/call평균 길이 100 -> 평균 시간: 0.125ns/바이트, 17.000ns/호출평균 길이 500 -> 평균 시간: 0.074ns/byte, 37.000ns/call평균 길이 1000 -> 평균 시간: 0.068ns/byte, 68.000ns/call평균 길이 5000 -> 평균 시간: 0.064ns/byte, 318.000ns/call평균 길이 10000 -> 평균 시간: 0.062ns/byte, 622.000ns/call평균 길이 1000000 -> 평균 시간: 0.062ns/byte, 62000.000ns/callchqrlie> gcc -std=c99 -O1 benchstrlen.c &&./a.out평균 길이 0 -> 평균 시간: 20.000ns/바이트, 20.000ns/호출평균 길이 4 -> 평균 시간: 3.818ns/byte, 21.000ns/call평균 길이 10 -> 평균 시간: 2.25ns/바이트, 23.000ns/호출평균 길이 50 -> 평균 시간: 0.990ns/바이트, 50.000ns/호출평균 길이 100 -> 평균 시간: 0.816ns/byte, 82.000ns/call평균 길이 500 -> 평균 시간: 0.679ns/byte, 340.000ns/call평균 길이 1000 -> 평균 시간: 0.664ns/byte, 664.000ns/call평균 길이 5000 -> 평균 시간: 0.651ns/byte, 3254.000ns/call평균 길이 10000 -> 평균 시간: 0.649ns/byte, 6491.000ns/call평균 길이 1000000 -> 평균 시간: 0.648ns/byte, 648000.000ns/callchqrlie> gcc -std=c99 -O2 benchstrlen.c &&./a.out평균 길이 0 -> 평균 시간: 10.000ns/byte, 10.000ns/call평균 길이 4 -> 평균 시간: 2.000ns/byte, 11.000ns/call평균 길이 10 -> 평균 시간: 1.048ns/byte, 11.000ns/call평균 길이 50 -> 평균 시간: 0.337ns/byte, 17.000ns/call평균 길이 100 -> 평균 시간: 0.155ns/바이트, 30.000ns/호출평균 길이 500 -> 평균 시간: 0.125ns/바이트, 101.000ns/호출평균 길이 1000 -> 평균 시간: 0.155ns/바이트, 188.000ns/호출평균 길이 5000 -> 평균 시간: 0.155ns/byte, 868.000ns/call평균 길이 10000 -> 평균 시간: 0.125ns/바이트, 1716.000ns/호출평균 길이 1000000 -> 평균 시간: 0.125ns/바이트, 172000.000ns/호출
GCC의 인라인 패턴은 의 16바이트 정렬을 감안할 때 SSE2 / , 및 로 할 수 있는 것보다 훨씬 느리다.이런 '최적화'는 사실 비관적이다.
16바이트 정렬의 장점을 살린 나의 간단한 손으로 쓴 루프는 gcc보다 5배 빠르다.-O3
대형 버퍼의 경우 인라인, 짧은 문자열의 경우 최대 2배 더 빠름. (짧은 문자열의 경우 strlen을 호출하는 것보다 빠름)https://gcc.gnu.org/bugzilla/show_bug.cgi?id=88809에 코멘트를 추가하여 gcc가 가능할 때 -O2 / -O3에서 인라인으로 해야 하는 것을 제안하였다. (시작할 4바이트 정렬만 알고 있을 경우 최대 16바이트까지 늘릴 것을 제안함)
gcc가 버퍼에 대한 4바이트 정렬을 알고 있을 때(보증:calloc
(), 인라인 선택strlen
GP 정수 레지스터를 사용하여 한 번에 4바이트 스칼라 비트백으로 사용 (-O2
그 이상이다.
(한 번에 4바이트를 읽는 것은 우리가 문자열 바이트가 들어 있지 않은 페이지로 건너갈 수 없다는 것을 알고, 따라서 맵이 풀릴 수 있다는 것을 알고 있을 때만 안전하다.x86과 x64의 같은 페이지 내에서 버퍼의 끝을 지나 읽어도 안전한가?(TL:DR yes, asm에서, 컴파일러는 C 소스에서 읽어도 그렇게 하는 코드를 내보낼 수 있다. libcstrlen
구현 또한 이를 활용한다.glibc에 대한 링크는 거기서 내 대답을 참조하십시오.strlen
그리고 큰 문자열에서 어떻게 그렇게 빨리 실행되는지 요약한다.)
에서는 항상(알 수 없는 정렬 상태에서도) 인라인을 로 선택하며, 이 속도는 매우 느리다(현대 Intel CPU의 클럭 사이클당 약 1바이트). "빠른 문자열"은 에만 적용된다.rep stos
그리고rep movs
,이 아니다.repz
/repnz
불행히도 지시사항이야그들의 마이크로코드는 한 번에 1바이트에 불과하지만 여전히 약간의 시동 오버헤드를 가지고 있다.(https://agner.org/optimize/)
(저장/재로드하여 컴파일러에서 포인터를 "히딩"하여 테스트할 수 있다.s
완전히volatile void *tmp
예를 들면.gcc는 포인터 값에 대해 0으로 가정해야 한다.volatile
, 정렬 정보 삭제)
GCC에는 다음과 같은 x86 튜닝 옵션이 있다.-mstringop-strategy=libcall
대unrolled_loop
대rep_byte
일반적으로 인라이닝 문자열 작업(스트롤링뿐만 아니라)memcmp
대표 또는 루프에 의해 수행될 수 있는 또 다른 주요한 것이 될 것이다.나는 이것들이 여기서 어떤 영향을 미치는지 확인하지 못했다.
다른 옵션에 대한 문서도 현재 동작을 설명한다.정렬되지 않은 포인터에서 원하는 경우에도 이 인라이닝(정렬 처리용 추가 코드 포함)을 얻을 수 있었다. (이것은 기계가 할 수 있는 것에 비해 인라인 루프가 쓰레기가 아닌 타겟에서 특히 작은 문자열의 경우 실제적인 성능의 승리였다.)
-minline-all-stringops
기본적으로 GCC는 대상이 최소 4바이트 경계로 정렬된 것으로 알려진 경우에만 문자열 연산을 줄인다.이것은 더 많은 인라이닝을 가능하게 하고 코드 크기를 증가시키지만, 짧은 시간 동안 빠른 memcpy, strlen, memset에 의존하는 코드의 성능을 향상시킬 수 있다.
GCC는 또한 다음과 같이 이것을 제어하기 위해 사용할 수 있는 기능별 속성을 가지고 있다.__attribute__((no-inline-all-stringops)) void foo() { ... }
, 그러나 나는 그것을 가지고 놀지 않았다. (그건 인라인올의 반대다.인라인 없음을 의미하는 것이 아니라 4바이트 정렬이 알려졌을 때만 인라인으로 되돌아간다.)
두 gcc 인라인strlen
전략은 16바이트 정렬을 활용하지 못하며 x86-64에는 매우 나쁘다.
작은 문자열 케이스가 매우 일반적인 경우가 아니라면, 하나의 4바이트 청크를 한 다음 정렬된 8바이트 청크는 4바이트보다 약 2배 더 빨리 처리될 것이다.
그리고 4바이트 전략은 0바이트를 포함하는 dword 내에서 바이트를 찾는 데 필요한 것보다 훨씬 느린 정리를 가지고 있다.하이비트가 설정된 바이트를 찾아 이를 감지하기 때문에 다른 비트를 마스크해 사용(비트 스캔 전진)하면 된다.이는 현대 CPU(Intel 및 Ryzen)에서 3주기 지연 시간을 가진다.또는 컴파일러가 사용할 수 있음rep bsf
로서 계속되다.tzcnt
BMI1을 지원하는 CPU에서 AMD에 더 효율적이다.bsf
그리고tzcnt
0이 아닌 입력에 대해 동일한 결과를 제공하십시오.
GCC의 4바이트 루프는 비트캔을 이용하지 않고 순수한 C, 즉 어떤 표적 독립 로직에서 컴파일된 것처럼 보인다.gcc는 정말 사용한다.andn
BMI1로 x86을 컴파일할 때 최적화할 수 있지만 사이클당 4바이트 미만이다.
SSE2 +는 짧은 입력과 긴 입력 모두에 훨씬 더 좋다.x86-64는 SSE2를 사용할 수 있음을 보장하며, x86-64 시스템 V는alignof(maxalign_t) = 16
그렇게calloc
최소 16바이트 정렬 포인터를 항상 반환한다.
대체를 썼다.strlen
성능을 테스트하기 위해 블록을 하다
역시 스카이레이크는 4바이트가 아니라 16바이트가 한 번에 4배 정도 빨라.
(원본을 asm에 정리했다.-O3
그런 다음, asm을 편집하여 이 인라인 확장 전략의 성능을 확인하십시오.strlen
또한 C 소스 내부의 인라인 asm에 포팅했다. Godbolt에 있는 버전을 보라.)
# at this point gcc has `s` in RDX, `i` in ECX
pxor %xmm0, %xmm0 # zeroed vector to compare against
.p2align 4
.Lstrlen16: # do {
#ifdef __AVX__
vpcmpeqb (%rdx), %xmm0, %xmm1
#else
movdqa (%rdx), %xmm1
pcmpeqb %xmm0, %xmm1 # xmm1 = -1 where there was a 0 in memory
#endif
add $16, %rdx # ptr++
pmovmskb %xmm1, %eax # extract high bit of each byte to a 16-bit mask
test %eax, %eax
jz .Lstrlen16 # }while(mask==0);
# RDX points at the 16-byte chunk *after* the one containing the terminator
# EAX = bit-mask of the 0 bytes, and is known to be non-zero
bsf %eax, %eax # EAX = bit-index of the lowest set bit
movb $'A', -16(%rdx, %rax)
strlen 정리의 일부를 스토어 주소 지정 모드로 최적화했다는 점에 유의하십시오.과대 촬영에 대한 수정은 다음과 같이 정정한다.-16
변위, 그리고 이것은 실제로 길이를 계산하고 GCC가 한 번에 4바이트의 루프를 줄인 후 이미 하고 있던 것처럼 인덱싱하는 것이 아니라, 문자열의 끝을 찾고 있는 것이다.
실제 문자열 길이를 얻으려면(끝에 대한 포인터 대신) rdx-start를 뺀 다음rax-16
(아마 레지스터 2개 + 상수를 추가하는 LEA가 있을 것이지만, 3-구성 요소 LEA는 더 많은 지연 시간을 갖는다.)
영점 레지스터를 파괴하지 않고 하나의 명령으로 load+compare를 허용하는 AVX의 경우 전체 루프는 5(테스트/jz 매크로 퓨즈가 Intel과 AMD 모두에서 하나의 op으로 감소)에서 4 ops에 불과하다.vpcmpeqb
비파괴 메모리 소스로 전체 파이프라인을 통해 마이크로프로그래밍이 가능하므로 프런트엔드용 퓨즈 도메인 웁은 1개뿐입니다.)
(128비트 AVX를 SSE와 혼합하면 처음부터 청정 상태에 있는 한 해스웰에서도 스톨이 발생하지 않는다는 점에 유의하십시오.그래서 나는 다른 지시사항들을 AVX로 바꾸는 것에 대해 신경쓰지 않았다. 단지 중요한 지시사항만.어떤 사소한 영향이 있는 것 같았다.pxor
사실 보다 조금 더 낫다.vpxor
하지만 내 바탕 화면에 AVX 루프 본체가 있어다소 반복할 수 있을 것 같았지만 코드 사이즈 차이가 없고 따라서 정렬 차이가 없어 이상하다.)
pmovmskb
단 한 번의 명령이다.인텔과 라이젠(불도저-패밀리의 경우 더 나빠짐)에서는 3주기 지연 시간을 가진다.짧은 문자열의 경우, SIMD 장치를 통과하여 정수로 되돌아가는 트립은 입력 메모리 바이트에서 준비 중인 저장 주소까지의 지연 시간을 위한 중요한 경로 종속성 체인의 중요한 부분이다.그러나 SIMD만이 팩트인더 비교를 하기 때문에 스칼라는 더 많은 일을 해야 할 것이다.
초소형 문자열 케이스(예: 0 ~ 3바이트)의 경우 순수 스칼라(특히 불도저 패밀리의 경우)를 사용하여 해당 사례에 대해 대기 시간을 약간 단축할 수 있지만, 0 ~ 15바이트의 모든 문자열이 동일한 분기 경로(루프 분기에서는 가져가지 않음)를 취하도록 하는 것은 대부분의 짧은 문자열 사용 사례에 매우 좋다.
우리가 16바이트 정렬을 가지고 있다는 것을 알고 있을 때, 15바이트까지의 모든 문자열에 매우 잘 어울리는 것은 좋은 선택인 것 같다.예측 가능한 분기가 매우 좋다. (그리고 루핑할 때,pmovmskb
지연 시간은 루프에서 벗어나기 위해 분기 오예측을 얼마나 빨리 감지할 수 있는가에만 영향을 미친다. 분기 예측 + 추측 실행은 각 반복에서 독립 pmovmskb의 지연 시간을 숨긴다.
더 긴 문자열이 일반화되기를 기대했다면 조금 더 길게 굴릴 수 있지만, 그 때 libc 함수를 호출하면 런타임에 AVX2로 전송될 수 있다.1개 이상의 벡터로 굴리면 정리가 복잡해져 간단한 케이스가 손상된다.
내 기계 i7-6700k 스카이레이크 4.2GHz 최대 터보(및energy_performance_preference
= 성능) Arch Linux에서 gcc8.2를 사용하면 CPU 클럭 속도가 mmset 동안 상승하기 때문에 벤치마크 타이밍이 다소 일관된다.그러나 항상 터보차저를 최대화하는 것은 아니다.스카이레이크의 hw 전력 관리는 메모리가 묶이면 중단된다. perf stat
내가 보통 4.0정도 맞았단걸 보여줬어GHz를 실행하면 stdout 출력을 평균화하고 stderr에 대한 성능 요약을 참조하십시오.
perf stat -r 100 ./a.out | awk '{sum+= $1} END{print sum/100;}'
나는 결국 내 asm을 GNU C 인라인-asm 문장으로 복사해 고드볼트 컴파일러 탐색기에 코드를 넣을 수 있게 되었다.
큰 문자열의 경우, 질문과 같은 길이: ~4GHz Skylake의 시간
- ~62100
clock_t
시간 단위:-O1
rep scas: (clock()
좀 구식이긴 하지만 굳이 바꾸지는 않았다.) - ~15900
clock_t
시간 단위:-O3
gcc 4바이트 루프 전략: 평균 100회 주행 = . (또는 ~15800회,-march=native
을 위해andn
) - ~1880
clock_t
시간 단위:-O3
glibc로strlen
함수 호출, AVX2 사용 - ~3190
clock_t
시간 단위: (AVX1 128비트 벡터, 4 uop 루프) gcc가 인라인할 수 있거나 인라인해야 하는 손으로 작성한 인라인 asm. - ~3230
clock_t
시간 단위: (SSE2 5 up 루프) gcc가 인라인으로 할 수 있거나 인라인으로 해야 하는 손으로 작성한 인라인 asm.
특별히 가지를 쳐야 할 필요가 없기 때문에 나의 손으로 쓴 am도 짧은 현에 매우 좋을 것이다.알려진 정렬은 스트렐린에게 매우 좋으며, libc는 그것을 이용할 수 없다.
큰 현이 드문 경우라고 예상한다면, 그 경우 lbc보다 1.7배 느리다.The length of 1M bytes means it won't be staying hot in L2 (256k) or L1d cache (32k) on my CPU, so even bottlenecked on L3 cache the libc version was faster. (Probably an unrolled loop and 256-bit vectors doesn't clog up the ROB with as many uops per byte, so OoO exec can see farther ahead and get more memory parallelism, especially at page boundaries.)
하지만 L3 캐시 대역폭은 아마도 4-up 버전이 클럭당 1번 반복으로 실행되는 것을 막는 병목 현상일 것이다. 따라서 AVX의 혜택을 덜 받고 있다. 따라서 AVX가 우리를 구해줄 것이다.L1d 캐시에 핫 데이터가 있는 경우 반복당 1.25 사이클이 1 사이클보다 클 것이다.
그러나 좋은 AVX2 구현은 다음을 사용하여 사이클당 최대 64바이트(2x 32바이트 로드)를 읽을 수 있다.vpminub
0을 확인하기 전에 쌍을 조합하고 0이 어디에 있는지 다시 찾아내는 것.L1d에서 뜨겁게 유지되는 약 2k에서 30 kiB 사이즈의 경우 이 값과 libc 사이의 간극이 더 넓게 열린다.
길이=1000의 일부 읽기 전용 테스트에서는 L1d 캐시에 핫한 중간 크기 문자열의 glibc가 실제로 내 루프보다 약 4배 빠르다는 것을 보여준다.이는 AVX2가 큰 미연속 루프까지 상승할 수 있을 만큼 충분히 크지만 여전히 L1d 캐시에 쉽게 들어맞는다. (읽기 전용으로 스토어 포워드 스톨을 피하고, 그래서 우리는 많은 반복을 할 수 있다.)
만약 당신의 문자열이 그렇게 크다면, 당신은 할 필요가 없이 명시적인 길이의 문자열을 사용해야 한다.strlen
따라서, 간단한 루프를 요약하는 것은 여전히 합리적인 전략처럼 보인다. 그것이 실제로 짧은 문자열에만 좋고 중간 (300바이트와 같은) 문자열과 매우 긴 (>캐시 크기) 문자열의 총 쓰레기가 아니라면 말이다.
이를 통해 소규모 문자열 벤치마킹:
예상한 결과를 얻으려다 이상한 점을 만났다.
나는 노력했다.s[31] = 0
반복하기 전에 문자열을 자른다(짧은 상수 길이).그러나 그때 나의 SSE2 버전은 GCC 버전과 거의 같은 속도였다.가게 앞 좌석은 병목현상이었습니다!바이트 저장소에 이어 로드가 넓어지면 저장소 전달이 저장소 버퍼의 바이트와 L1d 캐시의 바이트를 병합하는 느린 경로를 취하게 된다.이 추가 지연 시간은 다음 반복을 위한 저장소 인덱스를 계산하기 위해 문자열의 마지막 4바이트 또는 16바이트 청크를 통해 루프 캐리어 디포 체인의 일부다.
GCC의 느린 4바이트 코드는 그 지연 시간의 그늘에서 이전의 4바이트 청크를 처리함으로써 따라잡을 수 있었다. (순서가 맞지 않는 실행은 꽤 환상적이다: 느린 코드는 때때로 당신의 프로그램의 전체 속도에 영향을 주지 않을 수 있다.)
나는 결국 읽기 전용 버전을 만들고, 컴파일러 호스팅을 막기 위해 인라인 asm을 사용함으로써 그것을 해결했다.strlen
미개척의
그러나 스토어 포워딩은 16바이트 부하를 사용할 때 발생할 수 있는 잠재적인 문제다.다른 C 변수가 배열의 끝을 지나 저장될 경우, 우리는 더 좁은 스토어보다 더 멀리 배열의 끝을 로딩하여 SF 스톨을 칠 수 있다.최근에 복사된 데이터의 경우 16바이트 이상의 정렬된 스토어로 복사했다면 우리는 괜찮지만, 작은 복사본의 glibc memcpy는 개체의 시작과 끝에서 전체 객체를 덮는 2배의 중복 부하를 한다.그런 다음, 다시 겹쳐서, memmove src를 취급하는 것은 두 가지 모두를 무료로 저장한다.그래서 그냥 memcpyed된 짧은 문자열의 두 번째 16바이트 또는 8바이트 청크는 마지막 청크를 읽기 위한 SF 스톨을 줄지도 모른다.(출력에 대한 데이터 의존성이 있는 것)
단지 준비되기 전에 끝까지 가지 않도록 천천히 달리는 것은 일반적으로 좋지 않기 때문에, 여기에는 좋은 해결책이 없다.내 생각엔 당신이 방금 쓴 완충제를 사용하지 않을 때가 대부분이라고 생각한다. 보통 당신은 단지 읽기만 하는 입력물을 읽게 되기 때문에 가게 앞으로 가는 노점들은 문제가 되지 않는다.만약 다른 무언가가 그것을 썼다면, 효율적 코드는 희망컨대 길이를 버리고 그것을 다시 계산해야 하는 함수를 호출하지 않았을 것이다.
내가 제대로 파악하지 못한 또 다른 이상한 점들:
코드 정렬이 읽기 전용, 크기=1000에 대해 2개의 차이를 만드는 경우(s[1000] = 0;
그러나 가장 안쪽의 asm 루프 자체가.p2align 4
또는.p2align 5
.루프 정렬을 증가시키면 2배만큼 속도를 늦출 수 있다!
# slow version, with *no* extra HIDE_ALIGNMENT function call before the loop.
# using my hand-written asm, AVX version.
i<1280000 read-only at strlen(s)=1000 so strlen time dominates the total runtime (not startup overhead)
.p2align 5 in the asm inner loop. (32-byte code alignment with NOP padding)
gcc -DUSE_ASM -DREAD_ONLY -DHIDE_ALIGNMENT -march=native -O3 -g strlen-microbench.c &&
time taskset -c 3 perf stat -etask-clock,context-switches,cpu-migrations,page-faults,cycles,branches,branch-misses,instructions,uops_issued.any,uops_executed.thread -r 100 ./a.out |
awk '{sum+= $1} END{print sum/100;}'
Performance counter stats for './a.out' (100 runs):
40.92 msec task-clock # 0.996 CPUs utilized ( +- 0.20% )
2 context-switches # 0.052 K/sec ( +- 3.31% )
0 cpu-migrations # 0.000 K/sec
313 page-faults # 0.008 M/sec ( +- 0.05% )
168,103,223 cycles # 4.108 GHz ( +- 0.20% )
82,293,840 branches # 2011.269 M/sec ( +- 0.00% )
1,845,647 branch-misses # 2.24% of all branches ( +- 0.74% )
412,769,788 instructions # 2.46 insn per cycle ( +- 0.00% )
466,515,986 uops_issued.any # 11401.694 M/sec ( +- 0.22% )
487,011,558 uops_executed.thread # 11902.607 M/sec ( +- 0.13% )
0.0410624 +- 0.0000837 seconds time elapsed ( +- 0.20% )
40326.5 (clock_t)
real 0m4.301s
user 0m4.050s
sys 0m0.224s
노트 분기는 0이 아닌 것이 확실하며, 빠른 버전에 대해서는 거의 정확히 0이 아니다.그리고 발행된 ops는 빠른 버전보다 훨씬 더 높다: 그것은 각각의 분기 누락에 대해 오랫동안 잘못된 경로를 추측하고 있을 수 있다.
아마도 내측과 외측 루프 브랜치는 서로 별칭을 붙이거나 그렇지 않을 것이다.
명령 개수는 거의 동일하며, 단지 내부 루프 앞의 외부 루프에 있는 일부 NOP에 의해 다르다.그러나 IPC는 크게 다르다. 문제없이, 빠른 버전은 전체 프로그램에 대해 시계당 평균 4.82개의 지침을 실행한다.(이 중 대부분은 2개의 지침을 1 up으로 매크로 푸싱하는 테스트/jz 덕분에 사이클당 5개의 지침을 실행하는 가장 안쪽 루프에 있다.)그리고 ops_executive가 ops_isproval보다 훨씬 높다는 점에 유의하십시오. 이는 마이크로퓨전(micro-fusion)이 프런트 엔드 병목현상을 통해 ops를 더 많이 얻을 수 있도록 잘 작동하고 있다는 것을 의미한다.
fast version, same read-only strlen(s)=1000 repeated 1280000 times
gcc -DUSE_ASM -DREAD_ONLY -UHIDE_ALIGNMENT -march=native -O3 -g strlen-microbench.c &&
time taskset -c 3 perf stat -etask-clock,context-switches,cpu-migrations,page-faults,cycles,branches,branch-misses,instructions,uops_issued.any,uops_executed.thread -r 100 ./a.out |
awk '{sum+= $1} END{print sum/100;}'
Performance counter stats for './a.out' (100 runs):
21.06 msec task-clock # 0.994 CPUs utilized ( +- 0.10% )
1 context-switches # 0.056 K/sec ( +- 5.30% )
0 cpu-migrations # 0.000 K/sec
313 page-faults # 0.015 M/sec ( +- 0.04% )
86,239,943 cycles # 4.094 GHz ( +- 0.02% )
82,285,261 branches # 3906.682 M/sec ( +- 0.00% )
17,645 branch-misses # 0.02% of all branches ( +- 0.15% )
415,286,425 instructions # 4.82 insn per cycle ( +- 0.00% )
335,057,379 uops_issued.any # 15907.619 M/sec ( +- 0.00% )
409,255,762 uops_executed.thread # 19430.358 M/sec ( +- 0.00% )
0.0211944 +- 0.0000221 seconds time elapsed ( +- 0.10% )
20504 (clock_t)
real 0m2.309s
user 0m2.085s
sys 0m0.203s
그냥 가지 예측이지 다른 프런트엔드 물건들이 문제가 되지 않는 것 같아.테스트/지점 지침은 매크로 퓨전을 방지하는 경계선을 넘어 분할되지 않고 있다.
변화하는.p2align 5
로.p2align 4
되돌리기:-UHIDE_ALIGNMENT
더디게 되다
이 Godbolt 바이너리 링크는 Arch Linux에서 gcc8.2.1과 동일한 패딩을 2x11바이트로 재현한다.nopw
+ 3바이트nop
외부 루프 안쪽으로 빠르게 이동하십시오.그것은 또한 내가 현지에서 사용하던 정확한 출처를 가지고 있다.
짧은 strlen 읽기 전용 마이크로 스틱:
분기 오예측 또는 스토어 포워딩이 발생하지 않도록 선택한 항목으로 테스트하며, 의미 있는 데이터를 얻기에 충분한 반복을 위해 동일한 짧은 길이를 반복적으로 테스트할 수 있다.
strlen=33
그래서 터미네이터는 3번째 16바이트 벡터의 시작에 가깝다. (Make my version vs. 4바이트 vs. 가능한 한 나쁘게 보이게 만든다.) -DREAD_ONLY
그리고i<1280000
외측 반복 루프로서.
- 1933 clock_t: my asm: 멋지고 일관된 베스트 케이스 시간(평균을 다시 실행할 때 시끄럽지 않고 튕겨짐)포함/없음 동일 성능
-DHIDE_ALIGNMENT
긴 스트렐린과는 달리.루프 분기는 훨씬 더 짧은 패턴으로 훨씬 쉽게 예측할 수 있다. (stlen=33, 1000이 아닌) - 3220 clock_t: gcc -O3 call glibc
strlen
. (-DHIDE_ALIGNMENT
) - 6100 clock_t: gcc -O3 4바이트 루프
- 37200 clock_t: gcc -O1 repz scasb
짧은 문자열의 경우, 간단한 인라인 루프가 라이브러리 함수 호출에서strlen
PLT를 통과해야 하는 경우(콜 +)jmp [mem]
)) 그런 다음 정렬에 의존할 수 없는 Strlen의 시작 오버헤드를 실행하십시오.
모든 버전의 0.05%와 같이 무시할 수 있는 분기 예측이 있었다.strlen(s)=33
repz scasb 버전은 0.46%를 가지고 있었지만, 그것은 더 적은 수의 지점들 중 하나이다.정확하게 예측된 많은 가지를 고정할 수 있는 내부 루프가 없음.
분기 예측 변수와 코드 캐시 핫이 있는 경우, 33바이트 문자열을 glibc로 호출하는 것보다 10배 이상 더 나쁘다.실제 사용 사례에서 다음과 같은 경우 덜 나쁠 것이다.strlen
분기가 빗나가거나 심지어 빗나가서 코드 맞추기 게임과 스톨을 할 수도 있지만, 직진할 수도 있다.repz scasb
하지 않을하지만 10배는 거대하고, 그것은 꽤 짧은 끈을 위한 것이다.
'Programing' 카테고리의 다른 글
Vuejs 라우터, JavaScript 코드의 조건부 로딩 (0) | 2022.05.05 |
---|---|
store/의 Vuex Classic 모드는 사용되지 않으며 Nuxt 3에서 제거됨 (0) | 2022.05.05 |
읽기()와 recv()의 차이점과 보내기()와 쓰기()의 차이점은 무엇인가? (0) | 2022.05.04 |
Java에서 개인 정적 변수의 용도는 무엇인가? (0) | 2022.05.04 |
v-for를 사용하여 어레이의 일부만 반복하는 방법 (0) | 2022.05.04 |