Подтвердить что ты не робот

(Пропавших) улучшений производительности с семантикой перемещения С++ 11

Я писал код С++ 11 довольно долгое время и не проводил никакого бенчмаркинга, ожидая, что такие вещи, как векторные операции, будут "просто быстрее" с семантикой перемещения. Поэтому, когда на самом деле бенчмаркинг с GCC 4.7.2 и clang 3.0 (компиляторы по умолчанию на Ubuntu 12.10 64-bit), я получаю очень неудовлетворительные результаты. Это мой тестовый код:

РЕДАКТИРОВАТЬ: Что касается (хороших) ответов, отправленных @DeadMG и @ronag, я изменил тип элемента от std::string до my::string, который не имеет swap(), и сделал все внутренние строки большими (200-700 байт), чтобы они не стали жертвами единого входа.

EDIT2:. Адаптированный код снова по замечательным комментариям, изменил хранилище от std::string до std::vector<char> и оставил copy/move onstructors (позволяя компилятору сгенерировать их вместо этого). Без COW разница в скорости фактически огромна.

EDIT3: Повторно добавлено предыдущее решение при компиляции с помощью -DCOW. Это делает внутреннее хранилище a std::string, а не std::vector<char> по запросу @chico.

#include <string>
#include <vector>
#include <fstream>
#include <iostream>
#include <algorithm>
#include <functional>

static std::size_t dec = 0;

namespace my { class string
{
public:
    string( ) { }
#ifdef COW
    string( const std::string& ref ) : str( ref ), val( dec % 2 ? - ++dec : ++dec ) {
#else
    string( const std::string& ref ) : val( dec % 2 ? - ++dec : ++dec ) {
        str.resize( ref.size( ) );
        std::copy( ref.begin( ), ref.end( ), str.begin( ) );
#endif
    }

    bool operator<( const string& other ) const { return val < other.val; }

private:
#ifdef COW
    std::string str;
#else
    std::vector< char > str;
#endif
    std::size_t val;
}; }


template< typename T >
void dup_vector( T& vec )
{
    T v = vec;
    for ( typename T::iterator i = v.begin( ); i != v.end( ); ++i )
#ifdef CPP11
        vec.push_back( std::move( *i ) );
#else
        vec.push_back( *i );
#endif
}

int main( )
{
    std::ifstream file;
    file.open( "/etc/passwd" );
    std::vector< my::string > lines;
    while ( ! file.eof( ) )
    {
        std::string s;
        std::getline( file, s );
        lines.push_back( s + s + s + s + s + s + s + s + s );
    }

    while ( lines.size( ) < ( 1000 * 1000 ) )
        dup_vector( lines );
    std::cout << lines.size( ) << " elements" << std::endl;

    std::sort( lines.begin( ), lines.end( ) );

    return 0;
}

То, что это делает, читается /etc/passwd в вектор строк, а затем дублирует этот вектор на себя снова и снова, пока у нас не будет хотя бы 1 миллиона записей. Здесь первая оптимизация должна быть полезной, а не только явный std::move(), который вы видите в dup_vector(), но и push_back как таковой должен работать лучше, когда ему нужно изменить размер (создать новую + копию) внутреннего массива.

Наконец, вектор сортируется. Это определенно будет быстрее, если вам не нужно копировать временные объекты каждый раз, когда заменяются два элемента.

Я компилирую и запускаю эти два способа: один из них является С++ 98, а следующий - С++ 11 (с -DCPP11 для явного перемещения):

1> $ rm -f a.out ; g++ --std=c++98 test.cpp ; time ./a.out
2> $ rm -f a.out ; g++ --std=c++11 -DCPP11 test.cpp ; time ./a.out
3> $ rm -f a.out ; clang++ --std=c++98 test.cpp ; time ./a.out
4> $ rm -f a.out ; clang++ --std=c++11 -DCPP11 test.cpp ; time ./a.out

Со следующими результатами (дважды для каждой компиляции):

GCC C++98
1> real 0m9.626s
1> real 0m9.709s

GCC C++11
2> real 0m10.163s
2> real 0m10.130s

Итак, он немного медленнее запускается при компиляции кода С++ 11. Похожие результаты для clang:

clang C++98
3> real 0m8.906s
3> real 0m8.750s

clang C++11
4> real 0m8.858s
4> real 0m9.053s

Может кто-нибудь сказать мне, почему это? Оптимизируются ли компиляторы так хорошо даже при компиляции для pre-С++ 11, что они практически достигают семантического поведения перемещения в конце концов? Если я добавлю -O2, весь код работает быстрее, но результаты между разными стандартами почти такие же, как и выше.

EDIT: новые результаты с моими:: строками, а не std::string и большими отдельными строками:

$ rm -f a.out ; g++ --std=c++98 test.cpp ; time ./a.out
real    0m16.637s
$ rm -f a.out ; g++ --std=c++11 -DCPP11 test.cpp ; time ./a.out
real    0m17.169s
$ rm -f a.out ; clang++ --std=c++98 test.cpp ; time ./a.out
real    0m16.222s
$ rm -f a.out ; clang++ --std=c++11 -DCPP11 test.cpp ; time ./a.out
real    0m15.652s

Существуют небольшие небольшие различия между С++ 98 и C + 11 с семантикой перемещения. Чуть медленнее с С++ 11 с GCC и немного быстрее с clang, но все же очень маленькие различия.

EDIT2: Теперь без std::string COW, производительность значительно огромна:

$ rm -f a.out ; g++ --std=c++98 test.cpp ; time ./a.out
real    0m10.313s
$ rm -f a.out ; g++ --std=c++11 -DCPP11 test.cpp ; time ./a.out
real    0m5.267s
$ rm -f a.out ; clang++ --std=c++98 test.cpp ; time ./a.out
real    0m10.218s
$ rm -f a.out ; clang++ --std=c++11 -DCPP11 test.cpp ; time ./a.out
real    0m3.376s

С оптимизацией разница намного больше:

$ rm -f a.out ; g++ -O2 --std=c++98 test.cpp ; time ./a.out
real    0m5.243s
$ rm -f a.out ; g++ -O2 --std=c++11 -DCPP11 test.cpp ; time ./a.out
real    0m0.803s
$ rm -f a.out ; clang++ -O2 --std=c++98 test.cpp ; time ./a.out
real    0m5.248s
$ rm -f a.out ; clang++ -O2 --std=c++11 -DCPP11 test.cpp ; time ./a.out
real    0m0.785s

Выше показано, что с С++ 11 в 6-7 раз быстрее.

Спасибо за отличные комментарии и ответы. Я надеюсь, что этот пост будет полезен и интересен для других.

4b9b3361

Ответ 1

Это, безусловно, будет быстрее, если вам не нужно копировать временные объекты каждый раз, когда заменяются два элемента.

std::string имеет член swap, поэтому sort уже будет использовать его, и его внутренняя реализация уже будет воспринимать семантику эффективно. И вы не увидите разницы между копией и перемещением для std::string, пока задействовано SSO. Кроме того, некоторые версии GCC по-прежнему имеют версию COW на основе не С++ 11, которая также не видит большой разницы между копированием и перемещением.

Ответ 2

Вероятно, это связано с оптимизацией небольших строк, которая может возникать (в зависимости от компилятора) для строк короче, например, 16 символов. Я бы предположил, что все строки в файле довольно короткие, поскольку они являются паролями.

Если для конкретной строки активна небольшая оптимизация строк, перемещение выполняется как копия.

Вам понадобятся более крупные строки, чтобы увидеть улучшения скорости с помощью семантики перемещения.

Ответ 3

Думаю, вам нужно будет профилировать программу. Может быть, большую часть времени тратится на строки T v = vec; и std::sort(..) вектора из 20 миллионов строк!!! Нечего делать с семантикой перемещения.