다음은 매우 독특한 동작을 보여주는 C ++ 코드입니다. 이상한 이유로 데이터를 기적적으로 정렬하면 코드가 거의 6 배 빨라집니다.
#include <algorithm>
#include <ctime>
#include <iostream>
int main()
{
// Generate data
const unsigned arraySize = 32768;
int data[arraySize];
for (unsigned c = 0; c < arraySize; ++c)
data[c] = std::rand() % 256;
// !!! With this, the next loop runs faster.
std::sort(data, data + arraySize);
// Test
clock_t start = clock();
long long sum = 0;
for (unsigned i = 0; i < 100000; ++i)
{
// Primary loop
for (unsigned c = 0; c < arraySize; ++c)
{
if (data[c] >= 128)
sum += data[c];
}
}
double elapsedTime = static_cast<double>(clock() - start) / CLOCKS_PER_SEC;
std::cout << elapsedTime << std::endl;
std::cout << "sum = " << sum << std::endl;
}
- 없이
std::sort(data, data + arraySize);
코드가 11.54 초 안에 실행됩니다. - 정렬 된 데이터를 사용하면 코드가 1.93 초 안에 실행됩니다.
처음에는 이것이 언어 또는 컴파일러 이상일 수 있다고 생각했기 때문에 Java를 사용해 보았습니다.
import java.util.Arrays;
import java.util.Random;
public class Main
{
public static void main(String[] args)
{
// Generate data
int arraySize = 32768;
int data[] = new int[arraySize];
Random rnd = new Random(0);
for (int c = 0; c < arraySize; ++c)
data[c] = rnd.nextInt() % 256;
// !!! With this, the next loop runs faster
Arrays.sort(data);
// Test
long start = System.nanoTime();
long sum = 0;
for (int i = 0; i < 100000; ++i)
{
// Primary loop
for (int c = 0; c < arraySize; ++c)
{
if (data[c] >= 128)
sum += data[c];
}
}
System.out.println((System.nanoTime() - start) / 1000000000.0);
System.out.println("sum = " + sum);
}
}
비슷하지만 덜 극단적 인 결과.
첫 번째 생각은 정렬이 데이터를 캐시로 가져 오는 것이라고 생각했지만 배열이 방금 생성 되었기 때문에 얼마나 어리석은 지 생각했습니다.
- 무슨 일이야?
- 정렬되지 않은 배열을 처리하는 것이 정렬되지 않은 배열을 처리하는 것보다 빠른 이유는 무엇입니까?
코드는 독립적 인 용어를 요약하므로 순서는 중요하지 않습니다.
답변
분기 예측 실패 의 희생자입니다 .
분기 예측이란 무엇입니까?
철도 교차점을 고려하십시오.
Wikimedia Commons를 통한 Mecanismo의 이미지 . CC-By-SA 3.0 라이센스에 따라 사용됩니다 .
이제 논쟁의 여지가 있기 위해, 이것이 장거리 또는 무선 통신 전에 1800 년대에 다시 시작되었다고 가정하십시오.
당신은 정션의 운영자이며 기차가 오는 소리를 듣습니다. 당신은 그것이 어느 방향으로 가야할지 모른다. 열차를 멈 추면 운전자에게 원하는 방향을 물어볼 수 있습니다. 그런 다음 스위치를 적절하게 설정하십시오.
열차는 무겁고 많은 관성이 있습니다. 그래서 그들은 시작하고 느리게 영원히 걸립니다.
더 좋은 방법이 있습니까? 당신은 기차가 어느 방향으로 갈지 추측합니다!
- 당신이 올바르게 추측하면 계속됩니다.
- 당신이 틀렸다고 생각하면, 선장이 멈추고, 백업하고, 스위치를 뒤집으라고 소리 칠 것입니다. 그런 다음 다른 경로를 다시 시작할 수 있습니다.
당신이 매번 올바르게 추측한다면 , 기차는 결코 멈출 필요가 없습니다.
너무 자주 잘못 추측 하면 기차가 멈추고 백업하고 다시 시작하는 데 많은 시간이 걸립니다.
if 문을 고려하십시오 . 프로세서 레벨에서 이는 분기 명령입니다.
당신은 프로세서이고 지점을 볼 수 있습니다. 당신은 어떤 길로 갈지 모른다. 너 뭐하니? 실행을 중단하고 이전 지침이 완료 될 때까지 기다립니다. 그런 다음 올바른 경로를 계속 진행하십시오.
최신 프로세서는 복잡하고 파이프 라인이 길다. 그래서 그들은 “온난화”하고 “느리게”하는 데 영원히 걸립니다.
더 좋은 방법이 있습니까? 당신은 지점이 어느 방향으로 갈지 추측합니다!
- 올바르게 추측하면 계속 실행합니다.
- 잘못 추측 한 경우 파이프 라인을 플러시하고 분기로 롤백해야합니다. 그런 다음 다른 경로를 다시 시작할 수 있습니다.
매번 올바르게 추측 하면 실행을 중지 할 필요가 없습니다.
너무 자주 잘못 추측 하면 실속, 롤백 및 재시작에 많은 시간이 소요됩니다.
이것은 분기 예측입니다. 나는 기차가 깃발로 방향을 알릴 수 있기 때문에 그것이 가장 좋은 비유가 아니라는 것을 인정한다. 그러나 컴퓨터에서 프로세서는 지점이 마지막 순간까지 어느 방향으로 갈지 알 수 없습니다.
그렇다면 열차가 백업하고 다른 경로로 내려가는 횟수를 최소화하기 위해 어떻게 전략적으로 추측 할 것입니까? 당신은 과거의 역사를 봅니다! 기차가 시간의 99 %를 왼쪽으로 가면 왼쪽으로 추측됩니다. 그것이 번갈아 가면 추측을 번갈아 가며 나타냅니다. 세 번마다 한 가지 방법으로 진행된다면 같은 생각입니다.
다시 말해, 패턴을 식별하고 따르려고합니다. 이것은 분기 예측자가 작동하는 방식입니다.
대부분의 응용 프로그램에는 올바르게 동작하는 분기가 있습니다. 따라서 최신 지점 예측자는 일반적으로 90 % 이상의 적중률을 달성합니다. 그러나 인식 할 수있는 패턴이없는 예측할 수없는 분기에 직면 할 때 분기 예측기는 사실상 쓸모가 없습니다.
더 읽을 거리 : Wikipedia의 “지점 예측기”기사 .
위에서 암시 한 바와 같이 범인은이 if 문입니다.
if (data[c] >= 128)
sum += data[c];
데이터는 0과 255 사이에 균등하게 분배됩니다. 데이터를 정렬 할 때 대략 절반의 반복이 if 문에 들어 가지 않습니다. 그 후, 그들은 모두 if 문에 들어갑니다.
분기가 연속적으로 같은 방향으로 여러 번 이동하기 때문에 분기 예측 변수에 매우 친숙합니다. 간단한 포화 카운터조차도 방향 전환 후 몇 번의 반복을 제외하고 분기를 정확하게 예측합니다.
빠른 시각화 :
T = branch taken
N = branch not taken
data[] = 0, 1, 2, 3, 4, ... 126, 127, 128, 129, 130, ... 250, 251, 252, ...
branch = N N N N N ... N N T T T ... T T T ...
= NNNNNNNNNNNN ... NNNNNNNTTTTTTTTT ... TTTTTTTTTT (easy to predict)
그러나 데이터가 완전히 임의 인 경우 분기 예측기는 임의 데이터를 예측할 수 없으므로 쓸모가 없게됩니다. 따라서 약 50 %의 잘못된 예측이있을 것입니다 (임의 추측보다 낫지 않음).
data[] = 226, 185, 125, 158, 198, 144, 217, 79, 202, 118, 14, 150, 177, 182, 133, ...
branch = T, T, N, T, T, T, T, N, T, N, N, T, T, T, N ...
= TTNTTTTNTNNTTTN ... (completely random - hard to predict)
그래서 무엇을 할 수 있습니까?
컴파일러가 분기를 조건부 이동으로 최적화 할 수없는 경우 성능에 대한 가독성을 기꺼이 희생하려는 경우 해킹을 시도 할 수 있습니다.
바꾸다:
if (data[c] >= 128)
sum += data[c];
와:
int t = (data[c] - 128) >> 31;
sum += ~t & data[c];
이렇게하면 분기가 제거되고 일부 비트 단위 작업으로 대체됩니다.
(이 해킹은 원래 if 문과 완전히 동일하지는 않지만이 경우 모든 입력 값에 유효합니다 data[]
.)
벤치 마크 : Core i7 920 @ 3.5 GHz
C ++-Visual Studio 2010-x64 릴리스
// Branch - Random
seconds = 11.777
// Branch - Sorted
seconds = 2.352
// Branchless - Random
seconds = 2.564
// Branchless - Sorted
seconds = 2.587
Java-NetBeans 7.1.1 JDK 7-x64
// Branch - Random
seconds = 10.93293813
// Branch - Sorted
seconds = 5.643797077
// Branchless - Random
seconds = 3.113581453
// Branchless - Sorted
seconds = 3.186068823
관찰 :
- 지사와 함께 : 정렬 된 데이터와 정렬되지 않은 데이터 간에는 큰 차이가 있습니다.
- 해킹 사용 : 정렬 된 데이터와 정렬되지 않은 데이터 사이에는 차이가 없습니다.
- C ++의 경우, 데이터가 정렬 될 때 해킹은 실제로 분기보다 느리게 느립니다.
일반적인 경험 법칙은 중요한 루프에서 데이터 종속 분기를 피하는 것입니다 (이 예와 같이).
최신 정보:
-
x64가
-O3
있거나있는 GCC 4.6.1-ftree-vectorize
은 조건부 이동을 생성 할 수 있습니다. 따라서 정렬 된 데이터와 정렬되지 않은 데이터 사이에는 차이가 없습니다. 둘 다 빠릅니다.(또는 다소 빠름 : 이미 분류 된 경우,
cmov
특히 GCC 가 2주기 대기 시간이있는add
Broadwell 이전의 Intel에서 대신 GCC가 중요한 경로에 놓으면 특히 느려질 수 있습니다cmov
. gcc 최적화 플래그 -O3은 -O2보다 코드를 느리게합니다. ) -
VC ++ 2010은에서이 분기에 대한 조건부 이동을 생성 할 수 없습니다
/Ox
. -
ICC ( Intel C ++ Compiler ) 11은 기적적인 일을합니다. 그것은 두 개의 루프를 교환하는데 하여 외부 루프에 예측할 수 분기를 게양. 따라서 오해에 대한 내성이있을뿐만 아니라 VC ++ 및 GCC가 생성 할 수있는 것보다 두 배 빠릅니다! 다시 말해 ICC는 벤치 마크를 물리 치기 위해 테스트 루프를 활용했습니다.
-
인텔 컴파일러에 분기없는 코드를 제공하면 코드를 벡터화하여 분기와 마찬가지로 (루프 인터체인지) 빠릅니다.
이것은 성숙한 현대 컴파일러조차도 코드를 최적화하는 능력이 크게 다를 수 있음을 보여줍니다 …
답변
지점 예측.
정렬 된 배열의 경우 조건 data[c] >= 128
은 먼저 일련 false
의 값에 대한 조건 이며 true
이후의 모든 값에 적용됩니다. 예측하기 쉽습니다. 정렬되지 않은 배열을 사용하면 분기 비용을 지불합니다.
답변
데이터를 정렬 할 때 성능이 크게 향상되는 이유는 Mysticial의 답변 에서 아름답게 설명 된대로 분기 예측 패널티가 제거 되었기 때문 입니다.
이제 코드를 보면
if (data[c] >= 128)
sum += data[c];
이 특정 if... else...
분기 의 의미 는 조건이 충족 될 때 무언가를 추가 하는 것임을 알 수 있습니다 . 이 유형의 브랜치 는 시스템 에서 조건부 이동 명령 으로 쉽게 변환 될 수 있으며 조건부 이동 명령으로 컴파일됩니다 . 분기 및 잠재적 분기 예측 패널티가 제거됩니다.cmovl
x86
에서는 C
, 따라서 C++
,에서 조건부 이동 명령으로 (어떤 최적화없이) 직접 컴파일 할 문장은 x86
, 삼항 연산자입니다 ... ? ... : ...
. 따라서 위의 문장을 동등한 문장으로 다시 작성합니다.
sum += data[c] >=128 ? data[c] : 0;
가독성을 유지하면서 속도 향상 요소를 확인할 수 있습니다.
Intel Core i7 -2600K @ 3.4GHz 및 Visual Studio 2010 릴리스 모드에서 벤치 마크는 다음과 같습니다 (Mysticial에서 복사 한 형식).
x86
// Branch - Random
seconds = 8.885
// Branch - Sorted
seconds = 1.528
// Branchless - Random
seconds = 3.716
// Branchless - Sorted
seconds = 3.71
x64
// Branch - Random
seconds = 11.302
// Branch - Sorted
seconds = 1.830
// Branchless - Random
seconds = 2.736
// Branchless - Sorted
seconds = 2.737
결과는 여러 테스트에서 강력합니다. 분기 결과를 예측할 수 없을 때 속도가 크게 향상되지만 예측 가능할 때 약간의 어려움을 겪습니다. 실제로 조건부 이동을 사용하는 경우 데이터 패턴에 관계없이 성능이 동일합니다.
이제 x86
생성 한 어셈블리를 조사하여 더 자세히 살펴 보겠습니다 . 단순화하기 위해, 우리는 두 가지 기능을 사용 max1
하고max2
합니다.
max1
조건부 분기를 사용합니다 if... else ...
.
int max1(int a, int b) {
if (a > b)
return a;
else
return b;
}
max2
삼항 연산자를 사용합니다 ... ? ... : ...
.
int max2(int a, int b) {
return a > b ? a : b;
}
x86-64 시스템에서 GCC -S
아래 어셈블리를 생성하십시오.
:max1
movl %edi, -4(%rbp)
movl %esi, -8(%rbp)
movl -4(%rbp), %eax
cmpl -8(%rbp), %eax
jle .L2
movl -4(%rbp), %eax
movl %eax, -12(%rbp)
jmp .L4
.L2:
movl -8(%rbp), %eax
movl %eax, -12(%rbp)
.L4:
movl -12(%rbp), %eax
leave
ret
:max2
movl %edi, -4(%rbp)
movl %esi, -8(%rbp)
movl -4(%rbp), %eax
cmpl %eax, -8(%rbp)
cmovge -8(%rbp), %eax
leave
ret
max2
명령어 사용으로 인해 훨씬 적은 코드를 사용합니다 cmovge
. 그러나 실제 이득은 max2
지점 점프를 포함하지 않는다는 jmp
것인데, 이는 예측 된 결과가 올바르지 않으면 상당한 성능 저하를 초래할 수 있습니다.
그렇다면 조건부 이동이 더 나은 이유는 무엇입니까?
일반적인 x86
프로세서에서 명령 실행은 여러 단계로 나뉩니다. 대략, 우리는 다른 단계를 다루는 다른 하드웨어를 가지고 있습니다. 따라서 새로운 명령어를 시작하기 위해 하나의 명령어가 완료 될 때까지 기다릴 필요가 없습니다. 이것을 파이프 라이닝 이라고 합니다.
분기의 경우 다음 명령이 이전 명령에 의해 결정되므로 파이프 라이닝을 수행 할 수 없습니다. 기다리거나 예측해야합니다.
조건부 이동의 경우, 실행 조건부 이동 명령은 여러 단계로 나누어 지지만, 이전 단계 는 이전 명령의 결과 Fetch
와 유사 하며 그에 Decode
의존하지 않습니다. 후자의 단계 만 결과가 필요합니다. 따라서 하나의 명령 실행 시간의 일부를 기다립니다. 이것이 예측이 쉬운 경우 조건부 이동 버전이 분기보다 느린 이유입니다.
이 책은 컴퓨터 시스템 : 프로그래머의 관점은, 두 번째 버전은 자세하게 설명합니다. 조건부 이동 명령어에 대해서는 3.6.6 절 , 프로세서 아키텍처에 대해서는 4 장 , 분기 예측 및 오해에 대한 특별 처리에 대해서는 5.11.2 절을 확인할 수 있습니다 .
때로는 일부 최신 컴파일러가 더 나은 성능으로 어셈블리에 맞게 코드를 최적화 할 수 있고 때로는 일부 컴파일러에서는 그렇지 않을 수도 있습니다 (문제의 코드는 Visual Studio의 기본 컴파일러를 사용하고 있음). 예측할 수 없을 때 분기 및 조건부 이동 간의 성능 차이를 알면 시나리오가 너무 복잡해 컴파일러가 자동으로 최적화 할 수 없을 때 더 나은 성능으로 코드를 작성할 수 있습니다.
답변
이 코드에 대해 더 많은 최적화가 필요한 경우 다음 사항을 고려하십시오.
원래 루프로 시작 :
for (unsigned i = 0; i < 100000; ++i)
{
for (unsigned j = 0; j < arraySize; ++j)
{
if (data[j] >= 128)
sum += data[j];
}
}
루프 교환을 사용하면이 루프를 다음과 같이 안전하게 변경할 수 있습니다.
for (unsigned j = 0; j < arraySize; ++j)
{
for (unsigned i = 0; i < 100000; ++i)
{
if (data[j] >= 128)
sum += data[j];
}
}
그런 다음 루프 if
가 실행되는 동안 조건이 일정 하다는 것을 i
알 수 있으므로 if
아웃을 들어 올릴 수 있습니다 .
for (unsigned j = 0; j < arraySize; ++j)
{
if (data[j] >= 128)
{
for (unsigned i = 0; i < 100000; ++i)
{
sum += data[j];
}
}
}
그런 다음 부동 소수점 모델이 허용한다고 가정하면 내부 루프가 하나의 단일 표현식으로 축소 될 수 있음을 알 수 있습니다 ( /fp:fast
예 : 던져 짐)
for (unsigned j = 0; j < arraySize; ++j)
{
if (data[j] >= 128)
{
sum += data[j] * 100000;
}
}
그것은 이전보다 100,000 배 빠릅니다.
답변
의심 할 여지없이 우리 중 일부는 CPU의 분기 예측기에 문제가되는 코드를 식별하는 방법에 관심이있을 것입니다. Valgrind 도구 cachegrind
에는 --branch-sim=yes
플래그 를 사용하여 활성화되는 분기 예측기 시뮬레이터가 있습니다. 이 질문의 예제를 통해 실행하면 외부 루프 수가 10000으로 줄어들고로 컴파일되어 g++
다음과 같은 결과가 나타납니다.
정렬 :
==32551== Branches: 656,645,130 ( 656,609,208 cond + 35,922 ind)
==32551== Mispredicts: 169,556 ( 169,095 cond + 461 ind)
==32551== Mispred rate: 0.0% ( 0.0% + 1.2% )
분류되지 않은 :
==32555== Branches: 655,996,082 ( 655,960,160 cond + 35,922 ind)
==32555== Mispredicts: 164,073,152 ( 164,072,692 cond + 460 ind)
==32555== Mispred rate: 25.0% ( 25.0% + 1.2% )
cg_annotate
우리가 생성 한 라인 별 출력으로 드릴 다운하면 해당 루프가 나타납니다.
정렬 :
Bc Bcm Bi Bim
10,001 4 0 0 for (unsigned i = 0; i < 10000; ++i)
. . . . {
. . . . // primary loop
327,690,000 10,016 0 0 for (unsigned c = 0; c < arraySize; ++c)
. . . . {
327,680,000 10,006 0 0 if (data[c] >= 128)
0 0 0 0 sum += data[c];
. . . . }
. . . . }
분류되지 않은 :
Bc Bcm Bi Bim
10,001 4 0 0 for (unsigned i = 0; i < 10000; ++i)
. . . . {
. . . . // primary loop
327,690,000 10,038 0 0 for (unsigned c = 0; c < arraySize; ++c)
. . . . {
327,680,000 164,050,007 0 0 if (data[c] >= 128)
0 0 0 0 sum += data[c];
. . . . }
. . . . }
이를 통해 문제가있는 라인을 쉽게 식별 할 수 있습니다. 정렬되지 않은 버전에서는 if (data[c] >= 128)
라인이 캐시 그레인 Bcm
의 분기 예측기 모델에서 164,050,007 잘못 예측 된 조건부 분기 ( )를 야기하는 반면 정렬 된 버전에서는 10,006 만 발생합니다.
또는 Linux에서 성능 카운터 하위 시스템을 사용하여 동일한 작업을 수행 할 수 있지만 CPU 카운터를 사용하는 기본 성능을 사용할 수 있습니다.
perf stat ./sumtest_sorted
정렬 :
Performance counter stats for './sumtest_sorted':
11808.095776 task-clock # 0.998 CPUs utilized
1,062 context-switches # 0.090 K/sec
14 CPU-migrations # 0.001 K/sec
337 page-faults # 0.029 K/sec
26,487,882,764 cycles # 2.243 GHz
41,025,654,322 instructions # 1.55 insns per cycle
6,558,871,379 branches # 555.455 M/sec
567,204 branch-misses # 0.01% of all branches
11.827228330 seconds time elapsed
분류되지 않은 :
Performance counter stats for './sumtest_unsorted':
28877.954344 task-clock # 0.998 CPUs utilized
2,584 context-switches # 0.089 K/sec
18 CPU-migrations # 0.001 K/sec
335 page-faults # 0.012 K/sec
65,076,127,595 cycles # 2.253 GHz
41,032,528,741 instructions # 0.63 insns per cycle
6,560,579,013 branches # 227.183 M/sec
1,646,394,749 branch-misses # 25.10% of all branches
28.935500947 seconds time elapsed
또한 디스 어셈블리로 소스 코드 주석을 수행 할 수도 있습니다.
perf record -e branch-misses ./sumtest_unsorted
perf annotate -d sumtest_unsorted
Percent | Source code & Disassembly of sumtest_unsorted
------------------------------------------------
...
: sum += data[c];
0.00 : 400a1a: mov -0x14(%rbp),%eax
39.97 : 400a1d: mov %eax,%eax
5.31 : 400a1f: mov -0x20040(%rbp,%rax,4),%eax
4.60 : 400a26: cltq
0.00 : 400a28: add %rax,-0x30(%rbp)
...
자세한 내용 은 성능 자습서 를 참조하십시오.
답변
나는이 질문과 그 대답을 읽었으며 대답이 빠졌다고 느낍니다.
관리되는 언어에서 특히 잘 작동하는 것으로 알려진 분기 예측을 제거하는 일반적인 방법은 분기를 사용하는 대신 테이블 조회입니다 (이 경우 테스트하지는 않았지만).
이 방법은 다음과 같은 경우에 일반적으로 작동합니다.
- 작은 테이블이며 프로세서에 캐시 될 가능성이 높습니다.
- 아주 단단한 루프로 작업하고 있거나 프로세서가 데이터를 미리로드 할 수 있습니다.
배경과 이유
프로세서 관점에서 볼 때 메모리가 느립니다. 속도 차이를 보상하기 위해 프로세서에 두 개의 캐시가 내장되어 있습니다 (L1 / L2 캐시). 당신이 멋진 계산을하고 있다고 생각하고 메모리가 필요하다는 것을 알아 내십시오. 프로세서는 ‘로드’작업을 수행하고 메모리 조각을 캐시에로드 한 다음 캐시를 사용하여 나머지 계산을 수행합니다. 메모리가 상대적으로 느리기 때문에이 ‘로드’로 인해 프로그램 속도가 느려집니다.
브랜치 예측과 마찬가지로 Pentium 프로세서에서 최적화되었습니다. 프로세서는 데이터 조각을로드해야한다고 예측하고 작업이 실제로 캐시에 도달하기 전에 캐시에로드하려고 시도합니다. 우리가 이미 보았 듯이, 분기 예측은 때때로 끔찍하게 잘못됩니다. 최악의 시나리오에서는 되돌아 가서 실제로 메모리로드를 기다려야합니다. 이는 영원히 걸릴 것입니다 ( 즉, 분기 예측 실패가 잘못되었습니다, 메모리 분기 예측 실패 후로드는 끔찍합니다! ).
다행스럽게도 메모리 액세스 패턴을 예측할 수 있으면 프로세서가 빠른 캐시에로드하고 모든 것이 잘됩니다.
가장 먼저 알아야 할 것은 작은 것 입니까? 일반적으로 크기가 작을수록 좋지만, 일반적으로 <= 4096 바이트 인 조회 테이블을 사용하는 것이 좋습니다. 상한으로 : 룩업 테이블이 64K보다 크면 다시 고려해 볼 가치가 있습니다.
테이블 구성
그래서 우리는 작은 테이블을 만들 수 있다는 것을 알아 냈습니다. 다음으로해야 할 일은 찾아보기 기능을 사용하는 것입니다. 조회 함수는 일반적으로 몇 가지 기본 정수 연산 (및 xor, shift, 더하기, 제거 및 곱하기)을 사용하는 작은 함수입니다. 조회 기능에 의해 입력 내용을 테이블의 일종의 ‘고유 키’로 변환하여 원하는 모든 작업에 대한 답변을 제공합니다.
이 경우> = 128은 값을 유지할 수 있음을 의미하고 <128은 값을 제거함을 의미합니다. 가장 쉬운 방법은 ‘AND’를 사용하는 것입니다. 유지하면 7FFFFFFF로 AND합니다. 128을 2의 거듭 제곱으로하여 32768/128 정수의 테이블을 만들어 0과 1로 채울 수 있습니다. 7FFFFFFFF.
관리되는 언어
왜 이것이 관리 언어에서 잘 작동하는지 궁금 할 것입니다. 결국, 관리되는 언어는 분기로 배열의 경계를 확인하여 엉망이되지 않도록합니다 …
글쎄요, 정확히 … 🙂
관리되는 언어에 대한이 분기를 제거하는 작업이 꽤있었습니다. 예를 들면 다음과 같습니다.
for (int i = 0; i < array.Length; ++i)
{
// Use array[i]
}
이 경우 경계 조건이 절대로 맞지 않는 것은 컴파일러에게 명백합니다. 적어도 Microsoft JIT 컴파일러 (그러나 Java는 비슷한 작업을 수행한다고 생각합니다)는이를 확인하고 검사를 모두 제거합니다. 와우, 그것은 지점이 없다는 것을 의미합니다. 마찬가지로 다른 명백한 경우도 처리합니다.
관리되는 언어로 조회 할 때 문제가 발생하는 경우 핵심은 & 0x[something]FFF
경계 검사를 예측할 수 있도록 조회 기능에 추가하여 빠르게 진행하는 것입니다.
이 사건의 결과
// Generate data
int arraySize = 32768;
int[] data = new int[arraySize];
Random random = new Random(0);
for (int c = 0; c < arraySize; ++c)
{
data[c] = random.Next(256);
}
/*To keep the spirit of the code intact, I'll make a separate lookup table
(I assume we cannot modify 'data' or the number of loops)*/
int[] lookup = new int[256];
for (int c = 0; c < 256; ++c)
{
lookup[c] = (c >= 128) ? c : 0;
}
// Test
DateTime startTime = System.DateTime.Now;
long sum = 0;
for (int i = 0; i < 100000; ++i)
{
// Primary loop
for (int j = 0; j < arraySize; ++j)
{
/* Here you basically want to use simple operations - so no
random branches, but things like &, |, *, -, +, etc. are fine. */
sum += lookup[data[j]];
}
}
DateTime endTime = System.DateTime.Now;
Console.WriteLine(endTime - startTime);
Console.WriteLine("sum = " + sum);
Console.ReadLine();
답변
배열이 정렬 될 때 데이터가 0에서 255 사이로 분산되므로 반복의 절반 if
이- if
문에 입력되지 않습니다 ( 문은 아래에서 공유 됨).
if (data[c] >= 128)
sum += data[c];
문제는 정렬 된 데이터의 경우와 같이 특정 경우에 위의 문을 실행하지 못하게하는 이유는 무엇입니까? 여기에 “분기 예측기”가 있습니다. 브랜치 예측기는 브랜치 (예 : if-then-else
구조)가 어떤 방식으로 진행 될지 추측하기 위해 시도하는 디지털 회로입니다 . 분기 예측기의 목적은 명령 파이프 라인의 흐름을 개선하는 것입니다. 브랜치 예측자는 높은 효과적인 성능을 달성하는 데 중요한 역할을합니다!
더 잘 이해하기 위해 벤치 마킹을 해 봅시다.
-문의 성능은 if
해당 조건에 예측 가능한 패턴이 있는지 여부에 따라 다릅니다. 조건이 항상 참 또는 항상 거짓이면 프로세서의 분기 예측 논리가 패턴을 선택합니다. 다른 한편으로, 패턴이 예측할 수 if
없으면-문이 훨씬 비쌉니다.
다른 조건에서이 루프의 성능을 측정 해 봅시다 :
for (int i = 0; i < max; i++)
if (condition)
sum++;
다른 참-거짓 패턴을 가진 루프의 타이밍은 다음과 같습니다.
Condition Pattern Time (ms)
-------------------------------------------------------
(i & 0×80000000) == 0 T repeated 322
(i & 0xffffffff) == 0 F repeated 276
(i & 1) == 0 TF alternating 760
(i & 3) == 0 TFFFTFFF… 513
(i & 2) == 0 TTFFTTFF… 1675
(i & 4) == 0 TTTTFFFFTTTTFFFF… 1275
(i & 8) == 0 8T 8F 8T 8F … 752
(i & 16) == 0 16T 16F 16T 16F … 490
“ 나쁜 ”참-거짓 패턴은 if
“ 좋은 ”패턴 보다 최대 6 배 더 느리게 진술 할 수 있습니다 ! 물론 어떤 패턴이 좋고 나쁜지 컴파일러에 의해 생성 된 정확한 명령과 특정 프로세서에 따라 다릅니다.
따라서 분기 예측이 성능에 미치는 영향에 대해서는 의심의 여지가 없습니다!
