Измерение производительности вектора<unique_ptr> на VS2013?

TL;DR Неправильно ли оптимизатор VS2013 или мои измерения неверны или глобальный манекен действительно должен быть нестабильным, чтобы сделать тест достоверным или ____?

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


Введение: Некоторые недавние измерения привели меня к этому вопросу, потому что я увидел существенные различия между std::vector<std::unique_ptr<T> > а также boost::ptr_vector на VS2013. (также см. комментарии там)

Может показаться, что в моем конкретном тестовом примере доступ к элементам в boost::ptr_vector может быть на 50% быстрее, чем при использовании вектора unique_ptr!

Мой тестовый код находится здесь: /questions/18355537/rasschitat-otnositelnoe-vremya-v-c/18355543#18355543 (я также воздержусь от включения его в этот вопрос, я добавлю фрагменты ниже)

  • gcc 4.8 не сообщает о каких-либо различиях, так что это вещь VS2013.

    Start...
    The timings are as follows for accessing all (1000000) elements 200 times:
    * St6vectorISt10unique_ptrIjSt14default_deleteIjEESaIS3_EE: 1764 ms
    * N5boost10ptr_vectorIjNS_20heap_clone_allocatorESaIPvEEE: 1781 ms
    Dummy output: 500000
    
  • Мое время для именно тестового кода, с которым связаны:

    Start...
    The timings are as follows for accessing all (1.000.000) elements 200 times:
    * class std::vector<....>: 344 ms
    * class boost::ptr_vector<unsigned int,....>: 216 ms
    Dummy output: 500.000
    

Цикл тестирования выглядит следующим образом, я также оставлю там длинный комментарий, объясняющий, что я вижу:

template<typename C>
void RunContainerAccess(C& c) {
    for (size_t i = 0; i != loop_count; ++i) {
        for (auto& e : c) {
            // This is relevant: 
            // If the if-condition is present, VC++2013 will show 
            // approx. the same runtime for both cases. However,
            // if the only line in this loop is assigning the element
            // to the pGlobalDummy, *then* ptr_vector will run approx. 50%
            // faster than the unique_vector version!
            //
            // g++-4.8 does not show this behaviour
            //
            // Note: VS2013 commmand line: (release; /O2; no whole prg opt)
            //   /GS /analyze- /W3 /Gy /Zc:wchar_t /Zi /Gm- /O2 /sdl /Fd"Release\vc120.pdb" /fp:precise /D "WIN32" /D "NDEBUG" /D "_CONSOLE" /D "_LIB" /D "_UNICODE" /D "UNICODE" /errorReport:prompt /WX- /Zc:forScope /Gd /Oy- /Oi /MD /Fa"Release\" /EHsc /nologo /Fo"Release\" /Fp"Release\simple.pch" 
            //
            // Note: http://coliru.stacked-crooked.com/ command line:
            //   g++-4.8 -std=c++11 -O2 -Wall -pedantic -pthread main.cpp && ./a.out

            // if (pGlobalDummy)
                pGlobalDummy = PtrShim(e);
        }
    }
}

Если единственная строка в цикле обращается к элементу (помещая ptr в глобальную пустышку), то может показаться, что оптимизатор VS2013 делает что-то странное. Когда if (pGlobalDummy) присутствует, оба случая одинаковы.

Кто-нибудь может поделиться некоторым светом на это?

Благодаря ответу Говарда я обнаружил, что добавление volatile глобальный манекен имеет значение, то есть, когда глобальный манекен изменчив, как это:

extern MyType* volatile pGlobalDummy;
MyType* volatile pGlobalDummy = nullptr;

Циклы работают немного медленнее, но работают точно так же. Должен ли изменчивый иметь значение здесь? Таким образом, тест даже действителен без летучих?

1 ответ

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

Я использую кончик ствола + libC++ -O3.

Когда я запускаю ваш код без изменений, я получаю:

Start...
The timings are as follows for accessing all (1,000,000) elements 200 times:
* NSt3__16vectorINS_10unique_ptrIjNS_14default_deleteIjEEEENS_9allocatorIS4_EEEE: 0 ms
* N5boost10ptr_vectorIjNS_20heap_clone_allocatorENSt3__19allocatorIPvEEEE: 0 ms
Dummy output: 500,000

Я изменил выходные единицы на наносекунды и получил:

Start...
The timings are as follows for accessing all (1,000,000) elements 200 times:
* NSt3__16vectorINS_10unique_ptrIjNS_14default_deleteIjEEEENS_9allocatorIS4_EEEE: 32 ns
* N5boost10ptr_vectorIjNS_20heap_clone_allocatorENSt3__19allocatorIPvEEEE: 32 ns
Dummy output: 500,000

Подозрительно, я вставил изменчивый здесь:

extern MyType* <ins>volatile</ins> pGlobalDummy;
MyType* <ins>volatile</ins> pGlobalDummy = nullptr;

но без изменений.

Тогда я заметил, что time[2] не инициализируется, поэтому я:

chron::nanoseconds time[2]<ins> = {}</ins>;

Это сделал это. Теперь, установив единицы обратно в миллисекунды, я получаю:

Start...
The timings are as follows for accessing all (1,000,000) elements 200 times:
* NSt3__16vectorINS_10unique_ptrIjNS_14default_deleteIjEEEENS_9allocatorIS4_EEEE: 394 ms
* N5boost10ptr_vectorIjNS_20heap_clone_allocatorENSt3__19allocatorIPvEEEE: 406 ms
Dummy output: 500,000

Так что мне любопытно, если вы явно обнулите time[2]Вам может понадобиться:

chron::nanoseconds time[2] = {chron::nanoseconds(0), chron::nanoseconds(0)};

влияет ли это на результаты, которые вы видите?

осветление

std::chrono::duration конструктор по умолчанию указан как:

constexpr duration() = default;

Это будет по умолчанию инициализировать duration"s rep если клиент не указывает инициализацию списка, например:

chrono::nanoseconds ns;  // default-initialized

когда rep является арифметическим типом, инициализация не выполняется ([dcl.init]/p7/b3).

Если список клиентов инициализируется, например:

chrono::nanoseconds ns{};  // list-initialized

затем rep инициализируется значением ([dcl.init.list]/p3/b7), а для арифметических типов инициализация значения аналогична инициализации нуля ([dcl.init]/p8/b4).

Полный рабочий пример:

#include <iostream>
#include <chrono>

int
main()
{
    std::chrono::nanoseconds n1;
    std::chrono::nanoseconds n2{};
    std::chrono::nanoseconds n3 = {};
    std::cout << "n1 = " << n1.count() << "ns\n";
    std::cout << "n2 = " << n2.count() << "ns\n";
    std::cout << "n3 = " << n3.count() << "ns\n";
}

Для меня при компиляции с -O0 я получаю:

n1 = 0ns
n2 = 0ns
n3 = 0ns

Но компилируя то же самое с -O3, это меняется на:

n1 = 32ns
n2 = 0ns
n3 = 0ns
Другие вопросы по тегам