Один из моих сотрудников читал "Чистый код" Роберта К Мартина и попал в раздел о том, как использовать множество небольших функций, а не меньше больших функций. Это привело к дискуссии о последствиях этой методологии. Поэтому мы написали быструю программу для проверки производительности и сбиты с толку результатами.
Для начала здесь есть нормальная версия функции.
static double NormalFunction()
{
double a = 0;
for (int j = 0; j < s_OuterLoopCount; ++j)
{
for (int i = 0; i < s_InnerLoopCount; ++i)
{
double b = i * 2;
a = a + b + 1;
}
}
return a;
}
Вот версия, которую я сделал, которая разбивает функциональность на небольшие функции.
static double TinyFunctions()
{
double a = 0;
for (int i = 0; i < s_OuterLoopCount; i++)
{
a = Loop(a);
}
return a;
}
static double Loop(double a)
{
for (int i = 0; i < s_InnerLoopCount; i++)
{
double b = Double(i);
a = Add(a, Add(b, 1));
}
return a;
}
static double Double(double a)
{
return a * 2;
}
static double Add(double a, double b)
{
return a + b;
}
Я использую класс секундомера во время выполнения функций, и когда я запускал его в отладке, я получил следующие результаты.
s_OuterLoopCount = 10000;
s_InnerLoopCount = 10000;
NormalFunction Time = 377 ms;
TinyFunctions Time = 1322 ms;
Эти результаты имеют смысл для меня особенно в отладке, поскольку в вызовах функций есть дополнительные накладные расходы. Когда я запускаю его в выпуске, я получаю следующие результаты.
s_OuterLoopCount = 10000;
s_InnerLoopCount = 10000;
NormalFunction Time = 173 ms;
TinyFunctions Time = 98 ms;
Эти результаты путают меня, даже если компилятор оптимизирует TinyFunctions, объединив все вызовы функций, как это могло бы сделать это на 57% быстрее?
Мы пробовали объявления переменных переменных в NormalFunctions и в основном не влияли на время выполнения.
Я надеялся, что кто-то узнает, что происходит, и если компилятор может так хорошо оптимизировать TinyFunctions, почему он не может применять аналогичные оптимизации для NormalFunction.
Оглядываясь по сторонам, мы обнаружили, что кто-то упоминал о том, что разблокировка функций позволяет JIT лучше оптимизировать, что помещать в регистры, но NormalFunctions имеет только 4 переменные, поэтому мне трудно поверить, что объясняет огромную разницу в производительности.
Буду благодарен за любое понимание, которое кто-то может предоставить.
Обновление 1 Как было указано ниже, Кайл менял порядок операций, делал огромную разницу в производительности NormalFunction.
static double NormalFunction()
{
double a = 0;
for (int j = 0; j < s_OuterLoopCount; ++j)
{
for (int i = 0; i < s_InnerLoopCount; ++i)
{
double b = i * 2;
a = b + 1 + a;
}
}
return a;
}
Вот результаты с этой конфигурацией.
s_OuterLoopCount = 10000;
s_InnerLoopCount = 10000;
NormalFunction Time = 91 ms;
TinyFunctions Time = 102 ms;
Это больше того, что я ожидал, но все же оставляю вопрос о том, почему порядок операций может достигать производительности в 56%.
Кроме того, я затем попробовал его с целыми операциями, и мы вернулись к тому, чтобы не иметь никакого смысла.
s_OuterLoopCount = 10000;
s_InnerLoopCount = 10000;
NormalFunction Time = 87 ms;
TinyFunctions Time = 52 ms;
И это не изменяется независимо от порядка операций.