Почему std:: накапливается так медленно?
Я пытаюсь суммировать элементы массива, используя простой цикл цикла, std::accumulate
и ручную развертку для цикла. Как я полагаю, цикл с ручным разверткой является самым быстрым, но более интересным является то, что std:: accumulate намного медленнее, чем простой цикл.
Это мой код, я скомпилировал его с gcc 4.7 с флагом -O3. Visual Studio потребуется разная реализация функции rdtsc.
#include <iostream>
#include <algorithm>
#include <numeric>
#include <stdint.h>
using namespace std;
__inline__ uint64_t rdtsc() {
uint64_t a, d;
__asm__ volatile ("rdtsc" : "=a" (a), "=d" (d));
return (d<<32) | a;
}
class mytimer
{
public:
mytimer() { _start_time = rdtsc(); }
void restart() { _start_time = rdtsc(); }
uint64_t elapsed() const
{ return rdtsc() - _start_time; }
private:
uint64_t _start_time;
}; // timer
int main()
{
const int num_samples = 1000;
float* samples = new float[num_samples];
mytimer timer;
for (int i = 0; i < num_samples; i++) {
samples[i] = 1.f;
}
double result = timer.elapsed();
std::cout << "rewrite of " << (num_samples*sizeof(float)/(1024*1024)) << " Mb takes " << result << std::endl;
timer.restart();
float sum = 0;
for (int i = 0; i < num_samples; i++) {
sum += samples[i];
}
result = timer.elapsed();
std::cout << "naive:\t\t" << result << ", sum = " << sum << std::endl;
timer.restart();
float* end = samples + num_samples;
sum = 0;
for(float* i = samples; i < end; i++) {
sum += *i;
}
result = timer.elapsed();
std::cout << "pointers:\t\t" << result << ", sum = " << sum << std::endl;
timer.restart();
sum = 0;
sum = std::accumulate(samples, end, 0);
result = timer.elapsed();
std::cout << "algorithm:\t" << result << ", sum = " << sum << std::endl;
// With ILP
timer.restart();
float sum0 = 0, sum1 = 0;
sum = 0;
for (int i = 0; i < num_samples; i+=2) {
sum0 += samples[i];
sum1 += samples[i+1];
}
sum = sum0 + sum1;
result = timer.elapsed();
std::cout << "ILP:\t\t" << result << ", sum = " << sum << std::endl;
}
Ответы
Ответ 1
Во-первых, использование std::accumulate
является суммированием целых чисел.
Таким образом, вы, вероятно, оплачиваете стоимость конвертации каждого из
с плавающей точкой до целого, прежде чем добавлять его. Попробуйте:
sum = std::accumulate( samples, end, 0.0F );
и посмотрите, не влияет ли это.
Ответ 2
Поскольку вы (по-видимому) заботитесь об этом быстро, вы также можете подумать о попытке многопоточности вычислений, чтобы воспользоваться всеми доступными ядрами. Я сделал довольно тривиальный перебор вашего наивного цикла, чтобы использовать OpenMP, давая это:
timer.restart();
sum = 0;
// only real change is adding the following line:
#pragma omp parallel for schedule(dynamic, 4096), reduction(+:sum)
for (int i = 0; i < num_samples; i++) {
sum += samples[i];
}
result = timer.elapsed();
std::cout << "OMP:\t\t" << result << ", sum = " << sum << std::endl;
Просто для усмешек я также немного переписал ваш развернутый цикл, чтобы разрешить произвольное разворот, и добавив OpenMP:
static const int unroll = 32;
real total = real();
timer.restart();
double sum[unroll] = { 0.0f };
#pragma omp parallel for reduction(+:total) schedule(dynamic, 4096)
for (int i = 0; i < num_samples; i += unroll) {
for (int j = 0; j < unroll; j++)
total += samples[i + j];
}
result = timer.elapsed();
std::cout << "ILP+OMP:\t" << result << ", sum = " << total << std::endl;
Я также увеличил размер массива (по существу), чтобы получить несколько более значимые числа. Результаты были следующими. Сначала для двухъядерных процессоров AMD:
rewrite of 4096 Mb takes 8269023193
naive: 3336194526, sum = 536870912
pointers: 3348790101, sum = 536870912
algorithm: 3293786903, sum = 536870912
ILP: 2713824079, sum = 536870912
OMP: 1885895124, sum = 536870912
ILP+OMP: 1618134382, sum = 536870912
Затем для четырехъядерного процессора (Intel i7):
rewrite of 4096 Mb takes 2415836465
naive: 1382962075, sum = 536870912
pointers: 1675826109, sum = 536870912
algorithm: 1748990122, sum = 536870912
ILP: 751649497, sum = 536870912
OMP: 575595251, sum = 536870912
ILP+OMP: 450832023, sum = 536870912
Из взглядов на вещи версии OpenMP, вероятно, ограничивают пропускную способность памяти - версии OpenMP используют больше использования процессора, чем версии без потоковой передачи, но доходят только до 70% или около того, что указывает на некоторые кроме центрального процессора, выступает в роли узкого места.