Предсказание ветви: написание кода для его понимания; Получение странных результатов

Я пытаюсь получить хорошее представление о предсказании ветвлений, измеряя время выполнения циклов с предсказуемыми ветвями против циклов со случайными ветвями.

Поэтому я написал программу, которая принимает большие массивы 0 и 1, расположенные в разных порядках (т. Е. Все 0, повторяя 0-1, все ранды), и выполняет итерацию по разветвлению массива в зависимости от того, равен ли текущий индекс 0 или 1, и выполняет время. работы.

Я ожидал, что труднее угадать массивы потребуется больше времени для запуска, так как предиктор ветвления будет чаще угадывать ошибки, и что разница во времени между запусками на двух наборах массивов останется неизменной независимо от количества времени. тратить работу.

Тем не менее, по мере увеличения количества тратящихся времени, разница во времени между массивами увеличивалась, ОЧЕНЬ много.

Йо этот график не имеет смысла

(Ось X — это объем работы, потраченной впустую, ось Y — время выполнения)

Кто-нибудь понимает это поведение? Вы можете увидеть код, который я запускаю по следующему коду:

#include <stdlib.h>
#include <time.h>
#include <chrono>
#include <stdio.h>
#include <iostream>
#include <vector>
using namespace std;
static const int s_iArrayLen = 999999;
static const int s_iMaxPipelineLen = 60;
static const int s_iNumTrials = 10;

int doWorkAndReturnMicrosecondsElapsed(int* vals, int pipelineLen){
int* zeroNums = new int[pipelineLen];
int* oneNums = new int[pipelineLen];
for(int i = 0; i < pipelineLen; ++i)
zeroNums[i] = oneNums[i] = 0;

chrono::time_point<chrono::system_clock> start, end;
start = chrono::system_clock::now();
for(int i = 0; i < s_iArrayLen; ++i){
if(vals[i] == 0){
for(int i = 0; i < pipelineLen; ++i)
++zeroNums[i];
}
else{
for(int i = 0; i < pipelineLen; ++i)
++oneNums[i];
}
}
end = chrono::system_clock::now();
int elapsedMicroseconds = (int)chrono::duration_cast<chrono::microseconds>(end-start).count();

//This should never fire, it just exists to guarantee the compiler doesn't compile out our zeroNums/oneNums
for(int i = 0; i < pipelineLen - 1; ++i)
if(zeroNums[i] != zeroNums[i+1] || oneNums[i] != oneNums[i+1])
return -1;
delete[] zeroNums;
delete[] oneNums;
return elapsedMicroseconds;
}

struct TestMethod{
string name;
void (*func)(int, int&);
int* results;

TestMethod(string _name, void (*_func)(int, int&)) { name = _name; func = _func; results = new int[s_iMaxPipelineLen]; }
};

int main(){
srand( (unsigned int)time(nullptr) );

vector<TestMethod> testMethods;
testMethods.push_back(TestMethod("all-zero", [](int index, int& out) { out = 0; } ));
testMethods.push_back(TestMethod("repeat-0-1", [](int index, int& out) { out = index % 2; } ));
testMethods.push_back(TestMethod("repeat-0-0-0-1", [](int index, int& out) { out = (index % 4 == 0) ? 0 : 1; } ));
testMethods.push_back(TestMethod("rand", [](int index, int& out) { out = rand() % 2; } ));

int* vals = new int[s_iArrayLen];

for(int currentPipelineLen = 0; currentPipelineLen < s_iMaxPipelineLen; ++currentPipelineLen){
for(int currentMethod = 0; currentMethod < (int)testMethods.size(); ++currentMethod){
int resultsSum = 0;
for(int trialNum = 0; trialNum < s_iNumTrials; ++trialNum){
//Generate a new array...
for(int i = 0; i < s_iArrayLen; ++i)
testMethods[currentMethod].func(i, vals[i]);

//And record how long it takes
resultsSum += doWorkAndReturnMicrosecondsElapsed(vals, currentPipelineLen);
}

testMethods[currentMethod].results[currentPipelineLen] = (resultsSum / s_iNumTrials);
}
}

cout << "\t";
for(int i = 0; i < s_iMaxPipelineLen; ++i){
cout << i << "\t";
}
cout << "\n";
for (int i = 0; i < (int)testMethods.size(); ++i){
cout << testMethods[i].name.c_str() << "\t";
for(int j = 0; j < s_iMaxPipelineLen; ++j){
cout << testMethods[i].results[j] << "\t";
}
cout << "\n";
}
int end;
cin >> end;
delete[] vals;
}

Вставить ссылку: http://pastebin.com/F0JAu3uw

17

Решение

Я думаю, что вы, возможно, измеряете производительность кеша / памяти больше, чем предсказание ветвлений. Ваш внутренний цикл работы работает с постоянно растущим объемом памяти. Что может объяснить линейный рост, периодическое поведение и т. Д.

Я могу ошибаться, так как я не пытался воспроизвести ваши результаты, но на вашем месте я бы учел доступ к памяти, прежде чем рассчитывать другие события. Возможно, суммируйте одну переменную в другую вместо того, чтобы работать в массиве.

Также обратите внимание, что в зависимости от ЦП прогнозирование ветвления может быть намного умнее, чем просто запись последнего времени, когда ветвление было принято — например, повторяющиеся шаблоны не так плохи, как случайные данные.

Хорошо, быстрый и грязный тест, который я выполнил на своем перерыве на чай, который пытался отразить ваш собственный метод тестирования, но без перебора кэша, выглядит следующим образом:

введите описание изображения здесь

Это больше, чем вы ожидали?

Если я смогу сэкономить какое-то время позже, я захочу попробовать еще кое-что, поскольку я не особо задумывался о том, что делает компилятор …

Редактировать:

И вот мой последний тест — я перекодировал его в ассемблере, чтобы удалить ветвление цикла, обеспечить точное количество инструкций в каждом пути и т. Д.

Больше результатов прогноза отрасли

Я также добавил дополнительный случай 5-битного повторяющегося шаблона. Кажется, довольно сложно расстроить предсказатель ветвления моего стареющего Xeon.

20

Другие решения

В дополнение к тому, что указал JasonD, я также хотел бы отметить, что внутри for цикл, который может повлиять на прогнозирование ветвлений:

if(vals[i] == 0)
{
for(int i = 0; i < pipelineLen; ++i)
++zeroNums[i];
}

я < pipelineLen; это состояние, подобное вашему ifs. Конечно, компилятор может развернуть этот цикл, однако pipeLen — это аргумент, передаваемый функции, поэтому, вероятно, это не так.

Я не уверен, может ли это объяснить волнообразную картину ваших результатов, но:

Поскольку в процессоре Pentium 4 длина BTB составляет всего 16 записей, предсказание в конечном итоге не будет выполнено для циклов, которые длиннее 16 итераций. Этого ограничения можно избежать, развернув цикл, пока он не будет длиться всего 16 итераций. Когда это будет сделано, условное условие цикла всегда будет соответствовать BTB, и при выходе из цикла неправильное прогнозирование ветвления не произойдет. Ниже приведен пример развертывания цикла:

Читать статью полностью: http://software.intel.com/en-us/articles/branch-and-loop-reorganization-to-prevent-mispredicts

Таким образом, ваши циклы не только измеряют пропускную способность памяти, но и влияют на BTB.

Если вы прошли 0-1 шаблон в вашем списке, но затем выполнил цикл для pipelineLen = 2 ваш BTB будет заполнен чем-то вроде 0-1-1-0 - 1-1-1-0 - 0-1-1-0 - 1-1-1-0 и затем он начнет перекрываться, так что это действительно может объяснить волнообразную картину ваших результатов (некоторые перекрытия будут более вредными, чем другие).

Возьмите это как пример того, что может случиться, а не буквальное объяснение. Ваш ЦП может иметь гораздо более сложную архитектуру предсказания ветвлений.

2

По вопросам рекламы [email protected]