Подтвердить что ты не робот

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

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

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

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

Однако, по мере увеличения количества времени, затрачиваемого на работу, разница во времени между массивами увеличивалась, A LOT.

Yo this graph makes no sense

(ось 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;
}

Ссылка Pastebin: http://pastebin.com/F0JAu3uw

4b9b3361

Ответ 1

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

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

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

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

enter image description here

Чем больше вы ожидали?

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

Edit:

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

More branch prediction results

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

Ответ 2

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

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

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

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

Поскольку BTB занимает всего 16 записей в процессоре Pentium 4, предсказание в конечном итоге завершится неудачей для циклов, длина которых превышает 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, а затем он начнет перекрываться, так что это действительно может объяснить волновой рисунок ваших результатов (некоторые перекрытия будут более вредными, чем другие).

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