С++ массивная потеря производительности из-за оператора if

Я запускаю while loop в 4 потоках, в цикле я оцениваю функцию и постепенно увеличиваю счетчик.

while(1) {
    int fitness = EnergyFunction::evaluate(sequence);

    mutex.lock();
    counter++;
    mutex.unlock();
}

Когда я запускаю этот цикл, как я сказал в 4 потоках, я получаю ~ 20 000 000 оценок в секунду.

while(1) {
    if (dist(mt) == 0) {
        sequence[distDim(mt)] = -1;
    } else {
        sequence[distDim(mt)] = 1;
    }
    int fitness = EnergyFunction::evaluate(sequence);

    mainMTX.lock();
    overallGeneration++;
    mainMTX.unlock();
}

Если я добавлю некоторую случайную мутацию для последовательности, я получаю ~ 13 000 000 оценок в секунду.

while(1) {
    if (dist(mt) == 0) {
        sequence[distDim(mt)] = -1;
    } else {
        sequence[distDim(mt)] = 1;
    }
    int fitness = EnergyFunction::evaluate(sequence);

    mainMTX.lock();
    if(fitness < overallFitness)
        overallFitness = fitness;

    overallGeneration++;
    mainMTX.unlock();
}

Но когда я добавляю простую инструкцию if, которая проверяет, будет ли новый фитнес меньше старой, если это правда, замените старую пригодность новой пригодностью.

Но потеря производительности огромна! Теперь я получаю ~ 20 000 оценок в секунду. Если я удаляю случайную мутацию, я также получаю ~ 20 000 оценок в секунду.

Переменная totalFitness объявляется как

extern int overallFitness; 

У меня возникают проблемы с выяснением, в чем проблема такой большой потери производительности. Сравнивает ли два int такое время с операцией?

Также я не считаю, что это связано с блокировкой мьютекса.

UPDATE

Эта потеря производительности не была вызвана предсказанием ветвления, но компилятор просто проигнорировал этот вызов int fitness = EnergyFunction::evaluate(sequence);.

Теперь я добавил volatile, и компилятор больше не игнорирует вызов.

Также спасибо за указание на неверное предсказание отрасли и atomic<int>, не знали о них!

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

while(1) {
    sequence[distDim(mt)] = lookup_Table[dist(mt)];
    fitness = EnergyFunction::evaluate(sequence);
    if(fitness < overallFitness)
       overallFitness = fitness;
    ++overallGeneration;
}

Теперь я получаю ~ 25 000 оценок в секунду.

Ответ 1

Вам нужно запустить профилировщик, чтобы понять это. В Linux используйте perf.

Я предполагаю, что EnergyFunction::evaluate() полностью оптимизирован, потому что в первых примерах вы не используете результат. Таким образом, компилятор может отказаться от всего этого. Вы можете попробовать записать возвращаемое значение в переменную volatile, которая вынуждает компилятор или компоновщик не оптимизировать вызов. Ускорение 1000x определенно не связано с простым сравнением.

Ответ 2

На самом деле существует атомарная инструкция для увеличения int на 1. Таким образом, интеллектуальный компилятор может полностью удалить мьютекс, хотя я был бы удивлен, если бы это произошло. Вы можете протестировать это, посмотрев на сборку или удалив мьютекс и изменив тип overallGeneration на atomic<int>, проверьте, как быстро он все еще находится. Эта оптимизация больше невозможна с помощью вашего последнего, медленного примера.

Кроме того, если компилятор может видеть, что evaluate ничего не делает для глобального состояния, и результат не используется, он может пропустить весь вызов до evaluate. Вы можете узнать, смотря это на сборку или удалив вызов EnergyFunction::evaluate(sequence), и посмотрите время - если оно не ускорится, функция не была вызвана в первую очередь. Эта оптимизация больше невозможна с помощью вашего последнего, медленного примера. Вы должны иметь возможность остановить компилятор от выполнения EnergyFunction::evaluate(sequence) путем определения функции в другом объектном файле (другой cpp или библиотеке) и отключении оптимизации времени ссылки.

Здесь есть и другие эффекты, которые также создают разницу в производительности, но я не вижу других эффектов, которые могут объяснить разницу в коэффициенте 1000. Фактор 1000 обычно означает, что компилятор обманул предыдущий тест, и теперь изменение предотвращает это от обмана.

Ответ 3

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

В первом случае вы добавили ветки в некритичную область:

if (dist(mt) == 0) {
    sequence[distDim(mt)] = -1;
} else {
    sequence[distDim(mt)] = 1;
}

В этом случае CPU (по меньшей мере, IA) будет выполнять предсказание ветвления, а в случае предсказания пропусков ветки есть штраф за производительность - это известный факт.

Теперь, касаясь второго добавления, вы добавили ветвь в критическую область:

mainMTX.lock();
if(fitness < overallFitness)
    overallFitness = fitness;

overallGeneration++;
mainMTX.unlock();

В свою очередь, помимо штрафа "пропущенное предсказание" увеличилось количество кода, который выполняется в этой области, и, следовательно, вероятность того, что другим потокам придется ждать mainMTX.unlock();.

Примечание

Пожалуйста, убедитесь, что все глобальные/общие ресурсы определены как volatile. В противном случае компилятор может оптимизировать их (что может объяснить такое большое количество оценок в самом начале).

В случае overallFitness он, вероятно, не будет оптимизирован, потому что объявлен как extern, но overallGeneration может быть оптимизирован. Если это так, то это может объяснить это падение производительности после добавления "реального" доступа к памяти в критической области.

Примечание 2

Я все еще не уверен, что объяснение, которое я предоставил, может объяснить такое значительное снижение производительности. Поэтому я считаю, что могут быть некоторые детали реализации в коде, который вы не опубликовали (например, volatile).

ИЗМЕНИТЬ

Как Питер (@Peter) Mark Lakata​​strong > (@MarkLakata), указанный в отдельных ответах, и я склонен согласиться с ними, скорее всего, причина для падение производительности - это то, что в первом случае fitness никогда не использовался, поэтому компилятор просто оптимизировал эту переменную вместе с вызовом функции. В то время как во втором случае использовался fitness, поэтому компилятор не оптимизировал его. Хорошо поймите Петра и Марка! Я просто пропустил этот момент.

Ответ 4

Я понимаю, что это не является строго ответом на вопрос, а альтернативой представленной проблеме.

Используется ли overallGeneration во время работы кода? То есть, возможно ли это, чтобы определить, когда прекратить вычисление? Если это не так, вы можете отказаться от синхронизации глобального счетчика и иметь счетчик на поток, а после вычисления - суммировать все счетчики потоков на общую сумму. Аналогично для overallFitness вы можете отслеживать maxFitness за поток и выбирать максимум четырех результатов после завершения вычисления.

Не имея синхронизации нитей, вы получите 100% -ное использование ЦП.