Ответ 1
Так как код не делает ничего по-другому в каждой итерации цикла, компилятор может свободно перемещать код внутри цикла снаружи (результат будет точно таким же) и полностью удалить цикл, оставив вас почти 0 времени выполнения, как вы видели.
ОК, я говорил с другом о компиляторах и оптимизации программ, и он предположил, что n * 0.5
быстрее, чем n / 2
. Я сказал, что компиляторы делают такую оптимизацию автоматически, поэтому я написал небольшую программу, чтобы увидеть, существует ли разница между n / 2
и n * 0.5
:
Раздел:
#include <stdio.h>
#include <time.h>
int main(int argc, const char * argv[]) {
int i, m;
float n, s;
clock_t t;
m = 1000000000;
t = clock();
for(i = 0; i < m; i++) {
n = i / 2;
}
s = (float)(clock() - t) / CLOCKS_PER_SEC;
printf("n = i / 2: %d calculations took %f seconds (last calculation = %f)\n", m, s, n);
return 0;
}
Умножение:
#include <stdio.h>
#include <time.h>
int main(int argc, const char * argv[]) {
int i, m;
float n, s;
clock_t t;
m = 1000000000;
t = clock();
for(i = 0; i < m; i++) {
n = i * 0.5;
}
s = (float)(clock() - t) / CLOCKS_PER_SEC;
printf("n = i * 0.5: %d calculations took %f seconds (last calculation = %f)\n", m, s, n);
return 0;
}
И для обеих версий я получил 0.000002s avg. когда скомпилировано с clang main.c -O1
. И он сказал, что должно быть что-то не так с измерением времени. Затем он написал программу:
#include <cstdio>
#include <iostream>
#include <ctime>
using namespace std;
int main()
{
clock_t ts, te;
double dT;
int i, m;
double n, o, p, q, r, s;
m = 1000000000;
cout << "Independent calculations:\n";
ts = clock();
for (i = 0; i < m; i++)
{
// make it a trivial pure float calculation with no int casting to float
n = 11.1 / 2.3;
o = 22.2 / 2.3;
p = 33.3 / 2.3;
q = 44.4 / 2.3;
r = 55.5 / 2.3;
s = 66.6 / 2.3;
}
te = clock();
dT = ((float)(te - ts)) / CLOCKS_PER_SEC; // make initial call to get the elapsed time to run the loop
ts = clock();
printf("Division: %d calculations took %f seconds\n", m, dT);
for (i = 0; i < m; i++)
{
// make it a trivial pure float calculation with no int casting to float
n = 11.1 * 0.53;
o = 22.2 * 0.53;
p = 33.3 * 0.53;
q = 44.4 * 0.53;
r = 55.5 * 0.53;
s = 66.6 * 0.53;
}
te = clock();
dT = ((float)(te - ts)) / CLOCKS_PER_SEC; // make initial call to get the elapsed time to run the loop
ts = clock();
printf("Multiplication: %d calculations took %f seconds\n", m, dT);
cout << "\nDependent calculations:\n";
for (i = 0; i < m; i++)
{
// make it a trivial pure float calculation with no int casting to float
n = 11.1 / 2.3;
o = n / 2.3;
p = o / 2.3;
q = p / 2.3;
r = q / 2.3;
s = r / 2.3;
}
te = clock();
dT = ((float)(te - ts)) / CLOCKS_PER_SEC; // make initial call to get the elapsed time to run the loop
ts = clock();
printf("Division: %d calculations took %f seconds\n", m, dT);
for (i = 0; i < m; i++)
{
// make it a trivial pure float calculation with no int casting to float
n = 11.1 * 0.53;
o = n * 0.53;
p = o * 0.53;
q = p * 0.53;
r = q * 0.53;
s = r * 0.53;
}
te = clock();
dT = ((float)(te - ts)) / CLOCKS_PER_SEC; // make initial call to get the elapsed time to run the loop
ts = clock();
printf("Multiplication: %d calculations took %f seconds\n", m, dT);
return 0;
}
И для этого он получил...
1.869570s
1.868254s
25.674016s
3.497555s
... в этом порядке.
Итак, я запустил программу на своем компьютере, скомпилированном с помощью clang++ main.cpp -O1
, и получил аналогичные результаты: 0.000002 to 0.000011
.
Однако, когда я скомпилировал программу без оптимизации, я получил с ним аналогичные результаты в своем первом тесте. Итак, мой вопрос: как может любая оптимизация сделать программу намного быстрее?
Так как код не делает ничего по-другому в каждой итерации цикла, компилятор может свободно перемещать код внутри цикла снаружи (результат будет точно таким же) и полностью удалить цикл, оставив вас почти 0 времени выполнения, как вы видели.
Это хороший пример того, как бенчмаркинг языка высокого уровня еще сложнее, чем сборка эталонных тестов (что достаточно сложно, чтобы получить право уже). Компилятор может и часто будет мешать вашему эталону.
У вашего друга есть точка, деление (фактическое деление, а не только написание /
в C) медленнее, чем умножение. Для удвоений отношение составляет около 4 для задержки, а деление не конвейерно, а умножение - так, поэтому коэффициент пропускной способности намного хуже: около 20. (эти числа для Хасуэлла, но являются типичными)
Целочисленное деление медленнее, чем деление с плавающей запятой, но с использованием умножения с плавающей запятой на целое число вызывает два преобразования. Конверсии не так уж плохи, поэтому в целом умножение с плавающей запятой все еще быстрее.
Но любой правильный компилятор превратит целочисленное деление на константу в целочисленное умножение и сдвиг, а может быть, и некоторые дополнительные исправления (в зависимости от делителя и типа). Разделение на две силы еще проще. Подробнее см. Разделение на инвариантные целые числа с использованием умножения. В качестве примера рассмотрим
int div2(int i)
{
return i / 2;
}
GCC превращает это в
mov eax, edi
shr eax, 31
add eax, edi
sar eax
ret
который в зависимости от μarch будет занимать 3 или 4 цикла (исключая поток управления).
С другой стороны,
int div2(int i)
{
return i * 0.5;
}
Перевернут в
cvtsi2sd xmm0, edi
mulsd xmm0, QWORD PTR .LC0[rip]
cvttsd2si eax, xmm0
ret
.LC0:
.long 0
.long 1071644672
Что займет около 4 + 5 + 4 = 13 циклов.
Компилятору также разрешено превращать f / 2.0
в f * 0.5
даже без каких-либо "небезопасных оптимизаций", деление на две силы эквивалентно умножению на его обратное. Это не выполняется для чисел, которые не являются степенью двух.
Таким образом, даже с эталоном, который, по крайней мере, измерил что-то, он, вероятно, не измерил бы правильную вещь. Чтобы измерить деление с плавающей запятой с задержкой и умножением, вы можете написать что-то вроде:
.section data
align 16
one: dq 1.0, 1.0
.section text
_bench1:
mov ecx, -10000000
movapd xmm0, [one]
loopone:
mulpd xmm0, xmm0
mulpd xmm0, xmm0
add ecx, 1
jnz loopone
ret
_bench2:
mov ecx, -10000000
movapd xmm0, [one]
looptwo:
divpd xmm0, xmm0
divpd xmm0, xmm0
add ecx, 1
jnz looptwo
ret
Вызовите обе тысячи, завернутые в rdtsc
, чтобы получить время. Возьмите самое низкое время для обоих. Умножьте время на соотношение между базовыми тактами и турбо-часами. Затем вы должны иметь (приблизительно) количество циклов, которые выполняются как петлями, делятся на 20000000, чтобы получить количество циклов за mulpd
или divpd
. Временное деление зависит от деления значений, поэтому он не дает наиболее общего результата.
Вы также можете быть заинтересованы в списке задержек и производительности команд.
for (i = 0; i < m; i++)
{
// make it a trivial pure float calculation with no int casting to float
n = 11.1 * 0.53;
o = n * 0.53;
p = o * 0.53;
q = p * 0.53;
r = q * 0.53;
s = r * 0.53;
}
- это цикл, который не ссылается на i
или m
и не содержит круговых ссылок, поэтому для компилятора тривиально удалить оператор цикла