Почему valarray такой медленный?

Я пытаюсь использовать valarray, так как он очень похож на MATLAB при работе с векторами и матрицами. Сначала я проверил производительность и обнаружил, что valarray не может достичь заявленной производительности, как в книге Язык программирования C++ от Stroustrup.

Тестовая программа фактически выполнила 5 миллионов умножений двойного числа. Я думал, что c = a*b будет, по крайней мере, сравнимо с умножением элементов двойного типа цикла for, но я совершенно ошибался. Я пробовал на нескольких компьютерах и Microsoft Visual C++ 6.0 и Visual Studio 2008.

Кстати, я тестировал на MATLAB, используя следующий код:

len = 5*1024*1024;
a = rand(len, 1);
b = rand(len, 1);
c = zeros(len, 1);
tic;
c = a.*b;
toc;

И результат 46 мс. На этот раз нет высокой точности; он работает только как ссылка.

Код:

#include <iostream>
#include <valarray>
#include <iostream>
#include "windows.h"

using namespace std;
SYSTEMTIME stime;
LARGE_INTEGER sys_freq;

double gettime_hp();

int main()
{
    enum { N = 5*1024*1024 };
    valarray<double> a(N), b(N), c(N);
    QueryPerformanceFrequency(&sys_freq);
    int i, j;
    for (j=0 ; j<8 ; ++j)
    {
        for (i=0 ; i<N ; ++i)
        {
            a[i] = rand();
            b[i] = rand();
        }

        double* a1 = &a[0], *b1 = &b[0], *c1 = &c[0];
        double dtime = gettime_hp();
        for (i=0 ; i<N ; ++i)
            c1[i] = a1[i] * b1[i];
        dtime = gettime_hp()-dtime;
        cout << "double operator* " << dtime << " ms\n";

        dtime = gettime_hp();
        c = a*b ;
        dtime = gettime_hp() - dtime;
        cout << "valarray operator* " << dtime << " ms\n";

        dtime = gettime_hp();
        for (i=0 ; i<N ; ++i)
            c[i] = a[i] * b[i];
        dtime = gettime_hp() - dtime;
        cout << "valarray[i] operator* " << dtime<< " ms\n";

        cout << "------------------------------------------------------\n";
    }
}

double gettime_hp()
{
    LARGE_INTEGER tick;
    extern LARGE_INTEGER sys_freq;
    QueryPerformanceCounter(&tick);
    return (double)tick.QuadPart * 1000.0 / sys_freq.QuadPart;
}

Результаты работы: (режим релиза с максимальной оптимизацией скорости)

double operator* 52.3019 ms
valarray operator* 128.338 ms
valarray[i] operator* 43.1801 ms
------------------------------------------------------
double operator* 43.4036 ms
valarray operator* 145.533 ms
valarray[i] operator* 44.9121 ms
------------------------------------------------------
double operator* 43.2619 ms
valarray operator* 158.681 ms
valarray[i] operator* 43.4871 ms
------------------------------------------------------
double operator* 42.7317 ms
valarray operator* 173.164 ms
valarray[i] operator* 80.1004 ms
------------------------------------------------------
double operator* 43.2236 ms
valarray operator* 158.004 ms
valarray[i] operator* 44.3813 ms
------------------------------------------------------

Режим отладки с той же оптимизацией:

double operator* 41.8123 ms
valarray operator* 201.484 ms
valarray[i] operator* 41.5452 ms
------------------------------------------------------
double operator* 40.2238 ms
valarray operator* 215.351 ms
valarray[i] operator* 40.2076 ms
------------------------------------------------------
double operator* 40.5859 ms
valarray operator* 232.007 ms
valarray[i] operator* 40.8803 ms
------------------------------------------------------
double operator* 40.9734 ms
valarray operator* 234.325 ms
valarray[i] operator* 40.9711 ms
------------------------------------------------------
double operator* 41.1977 ms
valarray operator* 234.409 ms
valarray[i] operator* 41.1429 ms
------------------------------------------------------
double operator* 39.7754 ms
valarray operator* 234.26 ms
valarray[i] operator* 39.6338 ms
------------------------------------------------------

person shangping    schedule 27.07.2011    source источник
comment
Вы запускали исполняемый файл? или вы пробовали это в отладчике (через визуальную студию)?   -  person Yochai Timmer    schedule 28.07.2011
comment
Какие настройки оптимизации вы используете?   -  person Alan Stokes    schedule 28.07.2011
comment
Нет разницы ни в отладчике, ни в exe   -  person shangping    schedule 28.07.2011
comment
ВК6? Действительно? Ему 13 лет, и он предшествует стандарту.   -  person Alan Stokes    schedule 28.07.2011
comment
@alan: настройка: /nologo /ML /W3 /GX /O2 /D WIN32 /D NDEBUG /D _CONSOLE /D _MBCS /FpRelease/compvarray.pch /YX /FoRelease/ также скомпилировано на vs2008, работает так же   -  person shangping    schedule 28.07.2011
comment
Результаты на MSVS2010 различаются, но тренды те же: 17,5872, 51,9515, 25,5569.   -  person eugene_che    schedule 28.07.2011
comment
В GCC 4.6.1 с -flto -O3 -march=native -std=c++0x я получаю почти одинаковую производительность для всех трех случаев с небольшим увеличением от первого к третьему.   -  person Kerrek SB    schedule 28.07.2011
comment


Ответы (7)


Я подозреваю, что причина, по которой c = a*b выполняется намного медленнее, чем выполнение операций по одному элементу за раз, заключается в том, что

template<class T> valarray<T> operator*
    (const valarray<T>&, const valarray<T>&);

оператор должен выделить память для помещения результата, а затем возвращает его по значению.

Даже если для выполнения копирования используется «оптимизация подкачки», эта функция по-прежнему имеет накладные расходы

  • выделение нового блока для полученного valarray
  • инициализация нового valarray (возможно, это может быть оптимизировано)
  • помещая результаты в новый valarray
  • подкачка в памяти для нового valarray по мере его инициализации или установки с результирующими значениями
  • освобождение старого valarray, который заменяется результатом
person Michael Burr    schedule 27.07.2011
comment
Я только что искал, на самом деле он возвращает ссылку: template‹class _Ty› inline valarray‹_Ty›& operator*=(valarray‹_Ty›& _L, const _Ty& _R) {_VALGOP2(*= _R); } - person shangping; 28.07.2011
comment
В объявлении, которое вы разместили в комментарии выше, есть два отличия от того, что будет использоваться в коде, опубликованном в вопросе: 1) operator*= отличается от использования operator*(), за которым следует operator=(), и 2) это объявление для оператора *=, который принимает скалярный аргумент для умножения valarray на - person Michael Burr; 28.07.2011
comment
Михаил, согласно анализу, у нас нет возможности использовать valarray, если нужна производительность. Однако, согласно книге, этот класс специально разработан для повышения производительности. Не могли бы вы дать мне несколько очков? Любой другой метод, позволяющий мне обрабатывать массивы в целом, как valarray в С++? Спасибо - person shangping; 28.07.2011
comment
@Michael: вы можете быть правы, но посмотрите мои тесты Linux в отдельном ответе - определенно кажется, что производительность valarray не обязательно должна быть значительно хуже, чем у прямого цикла, при условии, что вы используете достойный компилятор и реализацию valarray. - person Paul R; 28.07.2011
comment
@shangping: Изначально предполагалось, что valarray может иметь специальные инструкции ЦП, такие как SSE. Они могут значительно увеличить скорость происходящего. Однако время копирования, которое вы испытываете здесь, намного перевешивает это преимущество, если ваша реализация вообще это делает. - person Puppy; 28.07.2011
comment
@shangping: извините, я бы не сильно помог с этой частью проблемы. Kerrick SB опубликовал комментарий, в котором говорится, что GCC 4.6.1 не показывает такой же деградации — может быть, использование MinGW может быть решением для вас? Другая возможность — использовать для этих операций собственную шаблонную функцию, которая принимает выходной параметр по ссылке. Это, вероятно, сделало бы ваш код более уродливым, чем вам хотелось бы. - person Michael Burr; 28.07.2011
comment
Новость: арифметика valarray разрешена, но не обязательна для использования шаблонов выражений (en. wikipedia.org/wiki/Expression_templates). Использование шаблонов выражений может полностью устранить временные проблемы в проблеме OP и, таким образом, полностью исключить выделение и освобождение кучи для выражения c = a*b. Видно, что это делает gcc (и слегка подправленный libc++), а MS C++ - нет. - person Howard Hinnant; 28.07.2011
comment
@howard, эта информация очень полезна. Я пытался сделать это, следуя инструкциям в книге, но я не могу сделать это правильно. Не могли бы вы показать мне «отложенную оценку» или шаблоны выражений специально для задачи c=a*b? Спасибо - person shangping; 28.07.2011
comment
@shangping: Программирование шаблонов выражений — одно из самых сложных упражнений на C++. Но статья в Википедии, на которую я ссылался выше, — неплохое начало. Пример VecDifference, который он использует, можно тривиально изменить на VecProduct. - person Howard Hinnant; 28.07.2011
comment
Жаль, что принятый ответ просто НЕПРАВИЛЬНЫЙ! Все, о чем вы говорите, просто НЕ ПРАВДА! Распределение всегда будет выполняться (для сохранения результата), и не будет никакой копии, поскольку любой приличный компилятор выполнит оптимизацию возвращаемого значения. Собственно говоря, с gcc нет существенной разницы между тремя реализациями... - person PierreBdR; 01.11.2013
comment
@PierreBdR, но причина, по которой gcc работает хорошо, не в оптимизации возвращаемого значения, а в том, что gcc valarray использует шаблоны выражений (см. комментарий Говарда Хиннанта выше). Это не из-за умной оптимизации компилятора, а из-за умной реализации Стандартной библиотеки std::valarray. - person Jonathan Wakely; 06.04.2018

Я только что попробовал это в системе Linux x86-64 (процессор Sandy Bridge):

gcc 4.5.0:

double operator* 9.64185 ms
valarray operator* 9.36987 ms
valarray[i] operator* 9.35815 ms

Intel ICC 12.0.2:

double operator* 7.76757 ms
valarray operator* 9.60208 ms
valarray[i] operator* 7.51409 ms

В обоих случаях я просто использовал -O3 и никаких других флагов, связанных с оптимизацией.

Похоже, что компилятор MS C++ и/или реализация valarray отстой.


Вот код OP, модифицированный для Linux:

#include <iostream>
#include <valarray>
#include <iostream>
#include <ctime>

using namespace std ;

double gettime_hp();

int main()
{
    enum { N = 5*1024*1024 };
    valarray<double> a(N), b(N), c(N) ;
    int i,j;
    for(  j=0 ; j<8 ; ++j )
    {
        for(  i=0 ; i<N ; ++i )
        {
            a[i]=rand();
            b[i]=rand();
        }

        double* a1 = &a[0], *b1 = &b[0], *c1 = &c[0] ;
        double dtime=gettime_hp();
        for(  i=0 ; i<N ; ++i ) c1[i] = a1[i] * b1[i] ;
        dtime=gettime_hp()-dtime;
        cout << "double operator* " << dtime << " ms\n" ;

        dtime=gettime_hp();
        c = a*b ;
        dtime=gettime_hp()-dtime;
        cout << "valarray operator* " << dtime << " ms\n" ;

        dtime=gettime_hp();
        for(  i=0 ; i<N ; ++i ) c[i] = a[i] * b[i] ;
        dtime=gettime_hp()-dtime;
        cout << "valarray[i] operator* " << dtime<< " ms\n" ;

        cout << "------------------------------------------------------\n" ;
    }
}

double gettime_hp()
{
    struct timespec timestamp;

    clock_gettime(CLOCK_REALTIME, &timestamp);
    return timestamp.tv_sec * 1000.0 + timestamp.tv_nsec * 1.0e-6;
}
person Paul R    schedule 27.07.2011
comment
Хорошо - просто для справки, не могли бы вы добавить параметры, используемые в сборке (я могу поиграть с этим сегодня вечером...) - person Michael Burr; 28.07.2011
comment
+1. Я запустил этот тест на реализации libc++. Он был не таким медленным, как MS, но и не таким быстрым, как gcc (это была примерно такая же скорость, как вы сообщаете для ICC). Оказывается, мне не хватало оператора присваивания клавиш в механизме шаблонов выражений. Добавил это. Теперь libc++ работает так же быстро, как gcc. ОП: Спасибо за тест скорости! (+1 к вопросу тоже) :-) - person Howard Hinnant; 28.07.2011
comment
Спасибо обоим - я добавил примечание о переключателях компилятора (только -O3 в обоих случаях), а также добавил код OP, модифицированный для Linux, который я использовал для этих тестов. - person Paul R; 28.07.2011

Весь смысл valarray в том, чтобы быть быстрым на векторных машинах, чего нет на машинах x86.

Хорошая реализация на невекторной машине должна соответствовать производительности, которую вы получаете с чем-то вроде

for (i=0; i < N; ++i) 
    c1[i] = a1[i] * b1[i];

а плохой, конечно, не будет. Если в аппаратном обеспечении нет ничего для ускорения параллельной обработки, это будет почти лучшее, что вы можете сделать.

person Leo Bellantoni    schedule 06.09.2011

Я, наконец, получил это с помощью отложенной оценки. Код может быть уродливым, так как я только начинаю изучать эти передовые концепции C++.

Вот код:

#include <iostream>
#include <valarray>
#include <iostream>
#include "windows.h"

using namespace std;
SYSTEMTIME stime;
LARGE_INTEGER sys_freq;

double gettime_hp();

// To improve the c = a*b (it will generate a temporary first, assigned to 'c' and delete the temporary.
// Which causes the program really slow
// The solution is the expression template and let the compiler to decide when all the expression is known.


// Delayed evaluation
//typedef valarray<double> Vector;
class Vector;

class VecMul
{
    public:
        const Vector& va;
        const Vector& vb;
        //Vector& vc;
        VecMul(const Vector& v1, const Vector& v2): va(v1), vb(v2) {}
        operator Vector();
};

class Vector:public valarray<double>
{
    valarray<double> *p;

    public:
        explicit Vector(int n)
        {
            p = new valarray<double>(n);
        }
        Vector& operator = (const VecMul &m)
        {
            for(int i=0; i<m.va.size(); i++)
                (*p)[i] = (m.va)[i]*(m.vb)[i]; // Ambiguous
            return *this;
        }
        double& operator[](int i) const {return (*p)[i];} //const vector_type[i]
        int size()const {return (*p).size();}
};


inline VecMul operator*(const Vector& v1, const Vector& v2)
{
    return VecMul(v1, v2);
}


int main()
{
    enum {N = 5*1024*1024};
    Vector a(N), b(N), c(N);
    QueryPerformanceFrequency(&sys_freq);
    int i, j;
    for (j=0 ; j<8 ; ++j)
    {
        for (i=0 ; i<N ; ++i)
        {
            a[i] = rand();
            b[i] = rand();
        }

        double* a1 = &a[0], *b1 = &b[0], *c1 = &c[0];
        double dtime = gettime_hp();
        for (i=0 ; i<N ; ++i)
            c1[i] = a1[i] * b1[i];
        dtime = gettime_hp()-dtime;
        cout << "double operator* " << dtime << " ms\n";

        dtime = gettime_hp();
        c = a*b;
        dtime = gettime_hp()-dtime;
        cout << "valarray operator* " << dtime << " ms\n";

        dtime = gettime_hp();
        for (i=0 ; i<N ; ++i)
            c[i] = a[i] * b[i];
        dtime = gettime_hp() - dtime;
        cout << "valarray[i] operator* " << dtime << " ms\n";

        cout << "------------------------------------------------------\n";
    }
}

double gettime_hp()
{
    LARGE_INTEGER tick;
    extern LARGE_INTEGER sys_freq;
    QueryPerformanceCounter(&tick);
    return (double)tick.QuadPart*1000.0/sys_freq.QuadPart;
}

Текущий результат в Visual Studio:

double operator* 41.2031 ms
valarray operator* 43.8407 ms
valarray[i] operator* 42.49 ms
person shangping    schedule 28.07.2011
comment
Почему у вашего class Vector есть два объекта valarray? Один как базовый класс, а другой выделен в куче? Кажется, вы используете только тот, который находится в куче, но для этого требуются дополнительные выделения. Просто используйте базовый класс! - person Jonathan Wakely; 06.04.2018

Я компилирую в выпуске x64, Visual Studio 2010. Я немного изменил ваш код:

    double* a1 = &a[0], *b1 = &b[0], *c1 = &c[0];
    double dtime = gettime_hp();
    for (i=0 ; i<N ; ++i)
        a1[i] *= b1[i];
    dtime = gettime_hp() - dtime;
    cout << "double operator* " << dtime << " ms\n";

    dtime = gettime_hp();
    a *= b;
    dtime = gettime_hp() - dtime;
    cout << "valarray operator* " << dtime << " ms\n";

    dtime = gettime_hp();
    for (i=0 ; i<N ; ++i)
        a[i] *= b[i];
    dtime = gettime_hp() - dtime;
    cout << "valarray[i] operator* " << dtime<< " ms\n";

    cout << "------------------------------------------------------\n" ;

Здесь вы можете видеть, что я использовал *= вместо c = a * b. В более современных математических библиотеках используются очень сложные механизмы шаблонов выражений, устраняющие эту проблему. В этом случае я на самом деле получил немного более быстрые результаты от valarray, хотя это, вероятно, только потому, что содержимое уже было в кеше. Накладные расходы, которые вы видите, являются просто избыточными временными параметрами и не имеют ничего общего с valarray, в частности, вы увидите такое же поведение с чем-то вроде std::string.

person Puppy    schedule 27.07.2011
comment
Я проверил ваши результаты. Однако это изменение не является незначительным изменением. Многие составные выражения не всегда могут быть выполнены с использованием *=, += /= - person shangping; 28.07.2011
comment
@shangping: В этом случае, если вы выделили новый массив результатов для каждой из необходимых вам временных переменных, вы увидите такое же замедление для double, как и для valarray. - person Puppy; 28.07.2011
comment
В более современных математических библиотеках используются очень сложные механизмы шаблонов выражений, которые устраняют эту проблему. А также в высококачественных реализациях std::valarray. - person Jonathan Wakely; 06.04.2018

Я думаю, что ответ Майкла Берра правильный. И, возможно, вы можете создать виртуальный тип как тип, возвращаемый оператором +, и перезагрузить другой operator= для этого виртуального типа, например operator=(virtual type& v){&valarray=&v;v=NULL;} (грубо говоря).

Конечно, реализовать идею на valarray сложно. Но когда вы создаете новый класс, вы можете попробовать эту идею. И потом, КПД у operator+ почти такой же, как у operator+=.

person Lin Xuelei    schedule 06.05.2015
comment
Это должен был быть комментарий к его ответу, а не новый ответ. - person Allan; 29.11.2017

Хм.. Я протестировал Blitz++, и это то же самое, что и valarray... И более того , оператор Blitz++ [] работает очень медленно.

#include <blitz/array.h>
#include <iostream>

#ifdef WIN32
#include "windows.h"
LARGE_INTEGER sys_freq;
#endif

#ifdef LINUX
<ctime>
#endif

using namespace std;
SYSTEMTIME stime;

__forceinline double gettime_hp();
double gettime_hp()
{
    #ifdef WIN32
        LARGE_INTEGER tick;
        extern LARGE_INTEGER sys_freq;
        QueryPerformanceCounter(&tick);
        return (double)tick.QuadPart * 1000.0 / sys_freq.QuadPart;
    #endif

    #ifdef LINUX
        struct timespec timestamp;

        clock_gettime(CLOCK_REALTIME, &timestamp);
        return timestamp.tv_sec * 1000.0 + timestamp.tv_nsec * 1.0e-6;
    #endif
}
BZ_USING_NAMESPACE(blitz)

int main()
{
    int N = 5*1024*1024;

    // Create three-dimensional arrays of double
    Array<double, 1> a(N), b(N), c(N);

    int i, j;

    #ifdef WIN32
        QueryPerformanceFrequency(&sys_freq);
    #endif

    for (j=0 ; j<8 ; ++j)
    {
        for (i=0 ; i<N ; ++i)
        {
            a[i] = rand();
            b[i] = rand();
        }

        double* a1 = a.data(), *b1 = b.data(), *c1 = c.data();
        double dtime = gettime_hp();
        for (i=0 ; i<N ; ++i)
            c1[i] = a1[i] * b1[i];
        dtime = gettime_hp() - dtime;
        cout << "double operator* " << dtime << " ms\n";

        dtime = gettime_hp();
        c = a*b;
        dtime = gettime_hp() - dtime;
        cout << "blitz operator* " << dtime << " ms\n";

        dtime = gettime_hp();
        for (i=0 ; i<N ; ++i)
            c[i] = a[i] * b[i];
        dtime = gettime_hp() - dtime;
        cout << "blitz[i] operator* " << dtime<< " ms\n";

        cout << "------------------------------------------------------\n";
    }
}
person user2778496    schedule 14.09.2013
comment
...и что в итоге? - person quant; 11.08.2014