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

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

Итак, я написал программу, которая берет большие массивы 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


person Ben Walker    schedule 04.01.2013    source источник
comment
Разве out = rand(); не должно быть out = rand() % 2;?   -  person David Schwartz    schedule 04.01.2013
comment
О боже, Дэвид, это была глупая ошибка с моей стороны. Отредактировано и дополнено новыми данными - это объясняет, почему случайный массив занял меньше времени, чем другие массивы.   -  person Ben Walker    schedule 04.01.2013
comment
Этот волнистый узор тоже интересен.   -  person doc    schedule 04.01.2013
comment
Кстати, какой тип процессора тестировался?   -  person doc    schedule 04.01.2013


Ответы (2)


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

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

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

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

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

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

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

Изменить:

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

Дополнительные результаты предсказания переходов

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

person JasonD    schedule 04.01.2013
comment
+1 за то, что вы действительно попробовали это сами - я полагаю, что 111 в легенде так же хороши, как 1 (т.е. всегда 1)? Если да, то это не кажется этим интересным, учитывая, что вы уже проверили всегда нулевой случай (и действительно, два графика в основном одинаковы). - person Frerich Raabe; 04.01.2013
comment
@FrerichRaabe На самом деле это должно быть «0111», но Excel обрезал метку, превратив текст в число ... - person JasonD; 04.01.2013
comment
О боже, это великолепные результаты - да, это именно то, что я ожидал увидеть (ну, я ожидал увидеть, что все линии имеют одинаковый наклон, не обязательно тот факт, что предсказатель ветвления достаточно умен в отношении повторяющихся шаблонов, чтобы тратить равные суммы). время на всех). Спасибо за информативный ответ! - person Ben Walker; 04.01.2013

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

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

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

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

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

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

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

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

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

person doc    schedule 04.01.2013
comment
Имейте в виду, что, как указано в тексте, который вы цитируете, этот 16-запись BTB относится к Pentium 4. Более современные процессоры имеют более продвинутую логику прогнозирования ветвлений. - person JasonD; 04.01.2013
comment
@JasonD, конечно, я не знаю, какой у тебя процессор. Возьмите это как пример того, что может произойти. BTB до сих пор работают по тому же принципу. Sandy Bridge, например, имеет один BTB. Статья от Intel от 2011 года, поэтому я думаю, что она все еще применима к более новым архитектурам. - person doc; 04.01.2013
comment
Да, и вы правы в том, что если вы хотите измерить предсказание ветвления изолированно, определенно стоит исключить ветки для циклов и т. Д. На моем локальном процессоре это немного очищает результаты, но в остальном не имеет большого влияния. . На ЦП OP это может иметь более глубокий эффект. - person JasonD; 04.01.2013
comment
@JasonD вот что я хочу выразить. Как это ни парадоксально, чтобы надежно измерить производительность вашей системы, вы должны хорошо знать ее архитектуру. В противном случае побочные эффекты могут исказить ваши результаты. - person doc; 04.01.2013