Вот небольшое любопытство микрооптимизации, которое я придумал:
struct Timer {
bool running{false};
int ticks{0};
void step_versionOne(int mStepSize) {
if(running) ticks += mStepSize;
}
void step_versionTwo(int mStepSize) {
ticks += mStepSize * static_cast<int>(running);
}
};
Похоже, два метода практически одинаковы. Избегает ли вторая версия ветвления (и, следовательно, быстрее, чем первая версия), или любой компилятор может выполнить такую оптимизацию с -O3
?
Да, ваш трюк позволяет избежать веток и делает это быстрее … иногда.
Я написал бенчмарк, который сравнивает эти решения в различных ситуациях, а также мои собственные:
ticks += mStepSize & -static_cast<int>(running)
Мои результаты следующие:
Off:
branch: 399949150
mul: 399940271
andneg: 277546678
On:
branch: 204035423
mul: 399937142
andneg: 277581853
Pattern:
branch: 327724860
mul: 400010363
andneg: 277551446
Random:
branch: 915235440
mul: 399916440
andneg: 277537411
Off
это когда таймеры выключены. В этом случае решения занимают примерно одинаковое время.
On
это когда они включены. Разветвление раствора в два раза быстрее.
Pattern
это когда они в шаблоне 100110. Производительность похожа, но ветвление немного быстрее.
Random
когда ветка непредсказуема. В этом случае умножение происходит более чем в 2 раза.
Во всех случаях мой бит-хакерский трюк самый быстрый, за исключением On
где ветвление побеждает.
Обратите внимание, что этот эталонный тест не обязательно является репрезентативным для всех версий процессоров компилятора и т. Д. Даже небольшие изменения эталонного теста могут перевернуть результаты с ног на голову (например, если компилятор может встроить знание mStepSize
является 1
чем умножение может быть на самом деле быстрее).
Код бенчмарка:
#include<array>
#include<iostream>
#include<chrono>
struct Timer {
bool running{false};
int ticks{0};
void branch(int mStepSize) {
if(running) ticks += mStepSize;
}
void mul(int mStepSize) {
ticks += mStepSize * static_cast<int>(running);
}
void andneg(int mStepSize) {
ticks += mStepSize & -static_cast<int>(running);
}
};
void run(std::array<Timer, 256>& timers, int step) {
auto start = std::chrono::steady_clock::now();
for(int i = 0; i < 1000000; i++)
for(auto& t : timers)
t.branch(step);
auto end = std::chrono::steady_clock::now();
std::cout << "branch: " << (end - start).count() << std::endl;
start = std::chrono::steady_clock::now();
for(int i = 0; i < 1000000; i++)
for(auto& t : timers)
t.mul(step);
end = std::chrono::steady_clock::now();
std::cout << "mul: " << (end - start).count() << std::endl;
start = std::chrono::steady_clock::now();
for(int i = 0; i < 1000000; i++)
for(auto& t : timers)
t.andneg(step);
end = std::chrono::steady_clock::now();
std::cout << "andneg: " << (end - start).count() << std::endl;
}
int main() {
std::array<Timer, 256> timers;
int step = rand() % 256;
run(timers, step); // warm up
std::cout << "Off:\n";
run(timers, step);
for(auto& t : timers)
t.running = true;
std::cout << "On:\n";
run(timers, step);
std::array<bool, 6> pattern = {1, 0, 0, 1, 1, 0};
for(int i = 0; i < 256; i++)
timers[i].running = pattern[i % 6];
std::cout << "Pattern:\n";
run(timers, step);
for(auto& t : timers)
t.running = rand()&1;
std::cout << "Random:\n";
run(timers, step);
for(auto& t : timers)
std::cout << t.ticks << ' ';
return 0;
}
Does the second version avoid a branch
если вы скомпилируете свой код, чтобы получить вывод на ассемблере, g++ -o test.s test.cpp -S
вы обнаружите, что во второй функции ветки действительно избегают.
and consequently, is faster than the first version
я выполнял каждую из твоих функций 2147483647
или же INT_MAX
количество раз, когда в каждой итерации я случайно назначил логическое значение running
член вашего Timer
структура, используя этот код:
int main() {
const int max = std::numeric_limits<int>::max();
timestamp_t start, end, one, two;
Timer t_one, t_two;
double percent;
srand(time(NULL));
start = get_timestamp();
for(int i = 0; i < max; ++i) {
t_one.running = rand() % 2;
t_one.step_versionOne(1);
}
end = get_timestamp();
one = end - start;
std::cout << "step_versionOne = " << one << std::endl;
start = get_timestamp();
for(int i = 0; i < max; ++i) {
t_two.running = rand() % 2;
t_two.step_versionTwo(1);
}
end = get_timestamp();
two = end - start;
percent = (one - two) / static_cast<double>(one) * 100.0;
std::cout << "step_versionTwo = " << two << std::endl;
std::cout << "step_one - step_two = " << one - two << std::endl;
std::cout << "one fast than two by = " << percent << std::endl;
}
и вот результаты, которые я получил:
step_versionOne = 39738380
step_versionTwo = 26047337
step_one - step_two = 13691043
one fast than two by = 34.4529%
так что да, вторая функция явно быстрее и примерно на 35%. обратите внимание, что процентное увеличение производительности по времени варьировалось от 30 до 55 процентов для меньшего числа итераций, тогда как, по-видимому, на более длительном этапе оно достигает 35%. это может быть связано со спорадическим выполнением системных задач во время выполнения симуляции, которое становится намного менее спорадическим, т.е. согласованным, чем дольше вы запускаете сим (хотя это только мое предположение, я понятия не имею, правда ли это)
В общем, хороший вопрос, я кое-что узнал сегодня!
конечно, случайно генерируя running
По сути, мы делаем предсказание ветвления бесполезным в первой функции, поэтому приведенные выше результаты не слишком удивительны. однако, если мы решим не изменять running
во время итераций цикла и вместо этого оставьте значение по умолчанию, в этом случае false
Предсказание ветвлений сделает свою магию в первой функции и на самом деле будет быстрее почти на 20%, как показывают эти результаты:
step_versionOne = 6273942
step_versionTwo = 7809508
step_two - step_one = 1535566
two fast than one by = 19.6628
так как running
постоянна на протяжении всего выполнения, обратите внимание, что время моделирования намного короче, чем при случайном изменении running
— вероятно, результат оптимизации компилятора.
почему вторая функция медленнее в этом случае? хорошо, предсказание ветвления быстро поймет, что условие в первой функции никогда не будет выполнено, и поэтому остановит проверку в первую очередь (как будто if(running) ticks += mStepSize;
даже не там). с другой стороны, второй функции все равно придется выполнять эту инструкцию ticks += mStepSize * static_cast<int>(running);
в каждой итерации, что делает первую функцию более эффективной.
но что если мы установим running
в true
? хорошо, предсказание ветвится снова, однако, на этот раз, первая функция должна будет оценить ticks += mStepSize;
в каждой итерации; здесь результаты, когда running{true}
:
step_versionOne = 7522095
step_versionTwo = 7891948
step_two - step_one = 369853
two fast than one by = 4.68646
заметить, что step_versionTwo
занимает одинаковое количество времени, будь то running
постоянно true
или же false
, но это все еще занимает больше времени, чем step_versionTwo
, однако незначительно. ну, это может быть потому, что я слишком ленив, чтобы запускать его много раз, чтобы определить, является ли он стабильно быстрым или одноразовым (случайные результаты немного меняются при каждом запуске, поскольку ОС должна работать в фоновом режиме). и это не всегда будет делать то же самое). но если это постоянно быстрее, это может быть потому, что функция два (ticks += mStepSize * static_cast<int>(running);
) имеет арифметический оп больше, чем функция один (ticks += mStepSize;
).
наконец, давайте скомпилируем с оптимизацией — g++ -o test test.cpp -std=c++11 -O1
и давайте вернемся running
вернуться к false
а затем проверьте результаты:
step_versionOne = 704973
step_versionTwo = 695052
более или менее то же самое. компилятор выполнит этап оптимизации и реализует running
всегда false
и, таким образом, для всех намерений и целей удалит тело step_versionOne
поэтому, когда вы вызываете его из цикла в main
, он просто вызовет функцию и вернется.
с другой стороны, при оптимизации второй функции он поймет, что ticks += mStepSize * static_cast<int>(running);
всегда будет генерировать один и тот же результат, т.е. 0
так что это тоже не будет беспокоить.
В общем, если я прав (а если нет, пожалуйста, поправьте меня, я довольно новичок в этом), все, что вы получите при вызове обеих функций из main
Цикл это их накладные расходы.
постскриптум вот результат для первого случая (running
генерируется случайным образом на каждой итерации) при компиляции с оптимизацией.
step_versionOne = 18868782
step_versionTwo = 18812315
step_two - step_one = 56467
one fast than two by = 0.299261