Ваш подход к сравнительному анализу в корне неверен, и ваш "осторожный код" является поддельным.
Во-первых, очистка кэша является поддельным.Мало того, что он будет быстро заполнен необходимыми данными, но также и опубликованные вами примеры имеют очень небольшое взаимодействие с памятью (только доступ к кешу call/ret
и нагрузка, к которой мы доберемся.
Во-вторых, уступить до того, как цикл сравнительного анализа окажется фиктивным. Вы выполняете итерацию 100000000 раз, что даже на достаточно быстром современном процессоре займет больше времени, чем обычные прерывания по расписанию в стандартной операционной системе. Если, с другой стороны, вы отключите планированиетактовые прерывания, а затем уступка до того, как тест ничего не сделает.
Теперь, когда бесполезная непредвиденная сложность исчезла, насчет фундаментального недопонимания современных процессоров:
Вы ожидаете loop_time_gross/loop_count
быть временем, затрачиваемым на каждую итерацию цикла. Это неправильно. Современные процессоры не выполняют команды последовательно, последовательно. Современные процессоры выполняют конвейеризацию, предсказывают переходы, выполняют несколько команд параллельно и (достаточно быстрые процессоры) выходят из строя.
Так что послеПервая горстка итераций цикла бенчмаркинга, все ветви идеально предсказаны для следующих почти 100000000 итераций.Это позволяет процессору спекулировать .Фактически, условная ветвь в цикле сравнительного анализа исчезает, как и большая часть стоимости косвенного вызова.Фактически, процессор может развернуть цикл:
movss xmm0, number
movaps xmm1, xmm0
rsqrtss xmm1, xmm0
mulss xmm0, xmm1
movss xmm0, number
movaps xmm1, xmm0
rsqrtss xmm1, xmm0
mulss xmm0, xmm1
movss xmm0, number
movaps xmm1, xmm0
rsqrtss xmm1, xmm0
mulss xmm0, xmm1
...
или, для другого цикла
movss xmm0, number
sqrtss xmm0, xmm0
movss xmm0, number
sqrtss xmm0, xmm0
movss xmm0, number
sqrtss xmm0, xmm0
...
Заметим, что загрузка number
всегда одинакова (таким образом, быстро кэшируется), и он перезаписывает только что вычисленное значение, разрывая цепочку зависимостей .
Если честно,
call rbp
sub rbx, 0x1
jne 15b0 <double profile(float)+0x20>
все еще выполняется ,но единственные ресурсы, которые они берут из кода с плавающей запятой, - это кэш декодирования / микрооперации и порты выполнения.Примечательно, что хотя код целочисленного цикла имеет цепочку зависимостей (обеспечивающую минимальное время выполнения), код с плавающей запятой не несет в себе зависимости .Кроме того, код с плавающей запятой состоит из множества взаимно полностью независимых коротких цепочек зависимостей.
Если вы ожидаете, что ЦП будет выполнять инструкции последовательно, ЦП может вместо этого выполнять их параллельно.
Небольшой взгляд на https://agner.org/optimize/instruction_tables.pdf показывает, почему это параллельное выполнение не работает для sqrtss
на Nehalem:
instruction: SQRTSS/PS
latency: 7-18
reciprocal throughput: 7-18
, т. Е. Инструкция не может быть передана по конвейеру и выполняется только наодин порт исполнения.Напротив, для movaps
, rsqrtss
, mulss
:
instruction: MOVAPS/D
latency: 1
reciprocal throughput: 1
instruction: RSQRTSS
latency: 3
reciprocal throughput: 2
instruction: MULSS
latency: 4
reciprocal throughput: 1
максимальная обратная пропускная способность цепочки зависимостей равна 2, поэтому можно ожидать, что код завершит выполнение одной цепочки зависимостей каждые 2циклы в устойчивом состоянии.На этом этапе время выполнения части цикла с плавающей запятой меньше или равно накладным расходам цикла и перекрывается с ним, поэтому ваш наивный подход к вычитанию накладных расходов цикла приводит к бессмысленным результатам.
Если вы хотите сделать это должным образом, вы должны убедиться, что отдельные итерации цикла зависят друг от друга, например, изменив свой цикл сравнения на
float x = INITIAL_VALUE;
for (i = 0; i < 100000000; i++)
x = benchmarked_function(x);
Очевидно, что вы не будете тестировать один и тот же вход таким образом, если INITIAL_VALUE
не является фиксированной точкой benchmarked_function()
.Однако вы можете организовать , чтобы она была фиксированной точкой расширенной функции, вычислив float diff = INITIAL_VALUE - benchmarked_function(INITIAL_VALUE);
и затем сделав цикл
float x = INITIAL_VALUE;
for (i = 0; i < 100000000; i++)
x = diff + benchmarked_function(x);
с относительно небольшими издержками, хотя тогда вам следуетубедитесь, что ошибки с плавающей точкой не накапливаются, чтобы существенно изменить значение, переданное в benchmarked_function()
.