Comments 44
А вот разницы между +it и it++ как раз и нет в этом конкретном тесте.
Берем уровень -O3 и сравниваем accumulate с самым хардкорным вариантом:
aleksey@aleksey ~/d/p/bench> ./test
t=0.00024414 ms
aleksey@aleksey ~/d/p/bench> ./test2
t=0.00024414 ms
А если не видно разницы, то зачем платить больше?
В свое время (ну лет 15 назад, когда DDR память появилась) AMD с Интелом выпускали километры документации о том как же быстро пройтись по массиву и получить весь заявленный в рекламе bandwidth. Ой не простое это было дело.
С тех пор подросло целое поколение…
Хочешь скорости работы программы — пиши в машинных кодах, а если хочешь скорости своей работы (и тех, кто будет потом править твой код), то пиши на максимально высокоуровневом языке =)
И это все, как и в случае со статьей, не учитывает оптимизацию компилятора.
Некоторый «сахар» C++ достается именно что бесплатно. При условии, что компилятор разберется, как перевести этот сахар в эффективный машинный код. Таких случаев немало, и особенно их немало с STL. В крайнем случае «платой за сахар» будет некоторое замедление компиляции.
И написание программы на ассемблере ничуть не уступает (по скорости ее работы) написанию ее в машинных кодах. Поэтому в машинных кодах никто не пишет еще с 50х-60х гг прошлого века.
польза статьи, скорее всего, в том, чтобы показать необходимость знать детали «работы» языка.
Вероятно, я невнимательно прочитал, потому что я не заметил описания каких-то деталей работы языка.
иначе эта опция был бы включена по умолчанию
Я думал, что оптимизации не включены по-умолчанию, чтобы сделать отладку проще и компиляцию быстрее (gcc docs), или другими словами сделать процесс разработки более удобным, разве нет?
Поначалу я удивился, почему помогает ключевое слово «register», ведь даже в VC5.0 компилятор разобрался бы, как оптимизировать такой простой цикл, и без «register». Сам бы на регистрах что надо разместил. Тем более регистров процессора более чем достаточно для такого простого цикла.
А еще автор не учел поведение кэша процессора. Измеренное быстродействие цикла может зависеть от того, что исполнял процессор перед этим циклом.
accumulate: 46 ms
for_each: 118 ms
for(int i: vec): 118 ms // Обидно, да?
for (auto i = vec.begin(); i != vec.end(); i++): 47 ms
for (auto i = vec.begin(); i != end; ++i): 46 ms
for (size_t i = 0; i < vec.size(); ++i) sum += vec[i];: 118 ms
for (size_t i = 0; i < size; ++i) sum += vec[i];: 47 ms // Ага, понятно, почему range-based for медленный: компилятор, как и в этом случае, не кэширует" vec.size() и вычисляет его каждый раз.
int *array = &vec[0]; for(size_t i = 0; i < size; ++i) sum += array[i];: 47 ms
auto end = &vec[vec.size() — 1]; for (int *i = &vec[0]; i != end; ++i) sum += *i;: 47 ms
register: 47 ms // Ч. т. д.
sum += array[i] + array[++i]: 65 ms // Интересно…
ассемблерный код получается следующим (VS2013, Release, x86)
mov eax,dword ptr[esp+0Ch]
mov esi,dword ptr[esp+8]
sub eax,esi
sar eax,2
Во как. 4 команды, 2 доступа к памяти, и кроме вычитания еще имеем сдвиг. Сейчас попробую сделать свой «вектор» и посмотреть, что накомпилирует компилятор при простом возврате члена данных класса.
#include <iostream>
#include <vector>
class kektor
{
public:
kektor(size_t nsize) : m_size(nsize) {}
size_t size(void) const throw() { return m_size; }
private:
size_t m_size;
};
void print_kek(const kektor& v)
{
for (size_t i = 0; i < v.size(); i++)
{
std::cout << i<<'\n';
}
}
void main(void)
{
std::cout << "Hello World!\n";
size_t sz;
std::cin >> sz;
kektor v(sz);
print_kek(v);
}
Ассемблерный код для функции print_kek:
xor edi,edi
test esi,esi
je endloop
loop:
push edi
call std::basic_ostream<char,std::char_traits<char> >::operator<<
mov ecx,eax
call std::operator<<<std::char_traits<char> >
inc edi
cmp edi,esi
jb loop
endloop: ...
Так что да. Неприятно. Недосмотр авторов STL. Оптимизации компилятора доверяй, но проверяй.
#include <vector>
#include <algorithm>
using namespace std;
int foo (vector<int> vec) {
return accumulate(vec.begin(), vec.end(), 0, [](int sum, int i) {
return sum + i;
});
}
int bar (vector<int> vec) {
const int COUNT = vec.size();
register int *array = &vec[0];
register int sum = 0;
for(register int i = 0; i != COUNT; ++i) {
sum += array[i];
};
return sum;
}
foo(std::vector<int, std::allocator<int> >):
mov rcx, QWORD PTR [rdi+8]
mov rdx, QWORD PTR [rdi]
xor eax, eax
cmp rdx, rcx
je .L4
.L3:
add eax, DWORD PTR [rdx]
add rdx, 4
cmp rcx, rdx
jne .L3
rep ret
.L4:
rep ret
bar(std::vector<int, std::allocator<int> >):
mov rcx, QWORD PTR [rdi]
mov rax, QWORD PTR [rdi+8]
sub rax, rcx
sar rax, 2
test eax, eax
mov esi, eax
je .L10
xor edx, edx
xor eax, eax
.L9:
add eax, DWORD PTR [rcx+rdx*4]
add rdx, 1
cmp esi, edx
jne .L9
rep ret
.L10:
xor eax, eax
ret
CLOCK_REALTIME
. Эти часы нельзя использовать для любых измерений производительности: документация явно говорит, что они могут изменяться. Специально для измерения производительности есть CLOCK_MONOTONIC_RAW (CLOCK_MONOTONIC для не‐linux) и CLOCK_PROCESS_CPUTIME_ID. На простоту кода изменение константы на правильную не влияет никак.sum += array[i] + array[++i];
Это явное UB? Я не удивляюсь, что там прирост скорости вдвое, там компилятор имел право вообще пол системы снести во время трансляции.
Упрощенный пример и предупреждение: codepad.org/U2jK5eIo
А вообще вот, что пишет стандарт:
If a side effect on a scalar object is unsequenced relative to either another side effect on the same scalar object or a value computation using the value of the same scalar object, the behavior is undefined.
В данном случае, как я понимаю, сайд эффект от ++i «unsequenced» с использованием i в array[i].
добавил в статью UPDATE, спасибо!
for(int *i = &vec[0]; i != end; ++i) {
sum += *i;
};
А последний элемент не суммируем?
g++ --version
cat /proc/cpuinfo | grep "model name" | head -n1
g++ -std=c++11 test_for.cpp -o test_for
g++ -std=c++11 -O3 test_for.cpp -o test_forO3
g++ -std=c++11 test_acc.cpp -o test_acc
g++ -std=c++11 -O3 test_acc.cpp -o test_accO3
g++ -std=c++11 test_reg.cpp -o test_reg
g++ -std=c++11 -O3 test_reg.cpp -o test_regO3
echo "No O; for,acc,reg:"
./test_for
./test_acc
./test_reg
echo "-O3; for,acc,reg:"
./test_forO3
./test_accO3
./test_regO3
Результат:
g++ (Ubuntu 4.9.2-0ubuntu1~14.04) 4.9.2
Copyright (C) 2014 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
model name : Intel(R) Core(TM) i7-4930K CPU @ 3.40GHz
No O; for,acc,reg:
t=11341 ms, sum=-2039607552
t=8141.1 ms, sum=-2039607552
t=660.89 ms, sum=-2039607552
-O3; for,acc,reg:
t=231.37 ms, sum=-2039607552
t=231.31 ms, sum=-2039607552
t=232.29 ms, sum=-2039607552
#include <iostream>
#include <sys/time.h>
#include <iomanip>
#include <vector>
#include <algorithm>
#include <numeric>
using namespace std;
const int COUNT = 1000 * 1000 * 1000;
int RandomNumber () {
static int s = 0;
return (s++ % 100);
}
int main () {
vector<int> vec(COUNT);
generate(vec.begin(), vec.end(), RandomNumber);
struct timespec tm1, tm2;
clock_gettime(CLOCK_REALTIME, &tm1);
register int *array = &vec[0];
register int sum = 0;
for(register int i = 0; i != COUNT; ++i) {
sum += array[i];
};
clock_gettime(CLOCK_REALTIME, &tm2);
double t1 = 1000.0 * tm1.tv_sec + tm1.tv_nsec / (1000.0 * 1000);
double t2 = 1000.0 * tm2.tv_sec + tm2.tv_nsec / (1000.0 * 1000);
cout << "t=" << setprecision(5) << t2 -t1 << " ms, sum=" <<sum<< endl;
return 0;
};
#include <iostream>
#include <sys/time.h>
#include <iomanip>
#include <vector>
#include <algorithm>
#include <numeric>
using namespace std;
const int COUNT = 1000 * 1000 * 1000;
int RandomNumber () {
static int s = 0;
return (s++ % 100);
}
int main () {
vector<int> vec(COUNT);
generate(vec.begin(), vec.end(), RandomNumber);
struct timespec tm1, tm2;
clock_gettime(CLOCK_REALTIME, &tm1);
int sum;
sum=accumulate(vec.begin(), vec.end(), 0, [](int sum, int i) {
return sum + i;
});
clock_gettime(CLOCK_REALTIME, &tm2);
double t1 = 1000.0 * tm1.tv_sec + tm1.tv_nsec / (1000.0 * 1000);
double t2 = 1000.0 * tm2.tv_sec + tm2.tv_nsec / (1000.0 * 1000);
cout << "t=" << setprecision(5) << t2 -t1 << " ms, sum=" <<sum<< endl;
return 0;
};
#include <iostream>
#include <sys/time.h>
#include <iomanip>
#include <vector>
#include <algorithm>
#include <numeric>
using namespace std;
const int COUNT = 1000 * 1000 * 1000;
int RandomNumber () {
static int s = 0;
return (s++ % 100);
}
int main () {
vector<int> vec(COUNT);
generate(vec.begin(), vec.end(), RandomNumber);
struct timespec tm1, tm2;
clock_gettime(CLOCK_REALTIME, &tm1);
int sum = 0;
for(auto i = vec.begin(); i != vec.end(); ++i) {
sum += *i;
};
clock_gettime(CLOCK_REALTIME, &tm2);
double t1 = 1000.0 * tm1.tv_sec + tm1.tv_nsec / (1000.0 * 1000);
double t2 = 1000.0 * tm2.tv_sec + tm2.tv_nsec / (1000.0 * 1000);
cout << "t=" << setprecision(5) << t2 -t1 << " ms, sum=" <<sum<< endl;
return 0;
};
for(int i = 0; i < siZEA; i++)
{
vec[i] = 25624;
}
0x401766 <+ 260> c7 45 f4 00 00 00 00 movl $0x0,-0xc(%ebp)
0x40176d <+ 267> 8b 45 f4 mov -0xc(%ebp),%eax
0x401770 <+ 270> 3b 45 e4 cmp -0x1c(%ebp),%eax
0x401773 <+ 273> 7d 1f jge 0x401794 MainWindow::MainWindow(QWidget*)+306
27 [1] vec[i] = 25624;
0x401775 <+ 275> 8b 45 f4 mov -0xc(%ebp),%eax
0x401778 <+ 278> 89 04 24 mov %eax,(%esp)
0x40177b <+ 281> b9 38 e0 40 00 mov $0x40e038,%ecx
0x401780 <+ 286> e8 6f 3e 00 00 call 0x4055f4 <QVector::operator>
0x401785 <+ 291> 83 ec 04 sub $0x4,%esp
0x401788 <+ 294> c7 00 18 64 00 00 movl $0x6418,(%eax)
25 [1] for(int i = 0; i < siZEA; ++i)
0x40178e <+ 300> 83 45 f4 01 addl $0x1,-0xc(%ebp)
0x401792 <+ 304> eb d9 jmp 0x40176d MainWindow::MainWindow(QWidget*)+267
25 [1] for(int i = 0; i < siZEA; i++)
{
vec[i] = 25624;
}
0x401766 <+ 260> c7 45 f4 00 00 00 00 movl $0x0,-0xc(%ebp)
0x40176d <+ 267> 8b 45 f4 mov -0xc(%ebp),%eax
0x401770 <+ 270> 3b 45 e4 cmp -0x1c(%ebp),%eax
0x401773 <+ 273> 7d 1f jge 0x401794 MainWindow::MainWindow(QWidget*)+306
27 [1] vec[i] = 25624;
0x401775 <+ 275> 8b 45 f4 mov -0xc(%ebp),%eax
0x401778 <+ 278> 89 04 24 mov %eax,(%esp)
0x40177b <+ 281> b9 38 e0 40 00 mov $0x40e038,%ecx
0x401780 <+ 286> e8 6f 3e 00 00 call 0x4055f4 <QVector::operator>
0x401785 <+ 291> 83 ec 04 sub $0x4,%esp
0x401788 <+ 294> c7 00 18 64 00 00 movl $0x6418,(%eax)
25 [1] for(int i = 0; i < siZEA; i++)
0x40178e <+ 300> 83 45 f4 01 addl $0x1,-0xc(%ebp)
0x401792 <+ 304> eb d9 jmp 0x40176d MainWindow::MainWindow(QWidget*)+267
++i i++ Разве есть сейчас в этом разница? Выше пример кода с ++i и i++ с одинаковым ассемблером. А ну еще сейчас for(auto it:vec) в 4-10 раз быстрее обычного цикла
Веселые старты или C++ и STL: кто быстрее?