std::move и оптимизация RVO

Недавно я прочитал, как std::move может ускорить код, просто перемещая значения вместо их копирования. Поэтому я сделал тестовую программу для сравнения скорости с использованием std::vector.

Код:

#include <iostream>
#include <vector>
#include <stdint.h>

#ifdef WIN32
#include <Windows.h>
#else
#include <sys/time.h>
#include <ctime>
#endif
#undef max

// Returns the amount of milliseconds elapsed since the UNIX epoch. Works on both
// windows and linux.

uint64_t GetTimeMs64()
{
#ifdef _WIN32
    // Windows
    FILETIME ft;
    LARGE_INTEGER li;

    // Get the amount of 100 nano seconds intervals elapsed since January 1, 1601 (UTC) and copy it
    // to a LARGE_INTEGER structure.
    GetSystemTimeAsFileTime(&ft);
    li.LowPart = ft.dwLowDateTime;
    li.HighPart = ft.dwHighDateTime;

    uint64_t ret = li.QuadPart;
    ret -= 116444736000000000LL; // Convert from file time to UNIX epoch time.
    ret /= 10000; // From 100 nano seconds (10^-7) to 1 millisecond (10^-3) intervals

    return ret;
#else
    // Linux
    struct timeval tv;

    gettimeofday(&tv, NULL);

    uint64 ret = tv.tv_usec;
    // Convert from micro seconds (10^-6) to milliseconds (10^-3)
    ret /= 1000;

    // Adds the seconds (10^0) after converting them to milliseconds (10^-3)
    ret += (tv.tv_sec * 1000);

    return ret;
#endif
}

static std::vector<std::string> GetVec1()
{
    std::vector<std::string> o(100000, "abcd");
    bool tr = true;
    if (tr)
        return std::move(o);
    return std::move(std::vector<std::string>(100000, "abcd"));
}

static std::vector<std::string> GetVec2()
{
    std::vector<std::string> o(100000, "abcd");
    bool tr = true;
    if (tr)
        return o;
    return std::vector<std::string>(100000, "abcd");
}

int main()
{
    uint64_t timer;
    std::vector<std::string> vec;

    timer = GetTimeMs64();
    for (int i = 0; i < 1000; ++i)
        vec = GetVec1();
    std::cout << GetTimeMs64() - timer << " timer 1(std::move)" << std::endl;
    timer = GetTimeMs64();
    for (int i = 0; i < 1000; ++i)
        vec = GetVec2();
    std::cout << GetTimeMs64() - timer << " timer 2(no move)" << std::endl;
    std::cin.get();
    return 0;
}

Я получил следующие результаты:

Выпуск (x86) /O2. tr = true

4376 таймер 1 (std::move)

4191 таймер 2(нет хода)

Выпуск (x86) /O2. tr = false

7311 таймер 1 (std::move)

7301 таймер 2 (без движения)

Результаты между двумя таймерами очень близки и не сильно отличаются. Я уже предположил, что это связано с оптимизацией возвращаемого значения (RVO), что означает, что мои возвращаемые значения уже перемещены компилятором без моего ведома, верно?

Затем я провел новые тесты без каких-либо оптимизаций, чтобы убедиться, что я прав. Результаты:

Выпуск (x86)/Od. tr = true

40860 таймер 1 (std::move)

40863 таймер 2(нет хода)

Выпуск (x86)/Od. tr = false

83567 таймер 1 (std::move)

82075 таймер 2 (без движения)

Теперь, несмотря на то, что разница между /O2 и /Od действительно значительна, разница между отсутствием движения или std::move (и даже между tr и true или false) минимальна.

Означает ли это, что хотя оптимизация отключена, компилятору разрешено применять RVO или std::move не так быстро, как я думал?


person Hatted Rooster    schedule 29.09.2015    source источник
comment
вы смотрели на сгенерированные инструкции?   -  person BeyelerStudios    schedule 29.09.2015
comment
Дополнительное примечание: вам следует изучить std::chrono::high_resolution_clock для ваших потребностей во времени...   -  person anorm    schedule 29.09.2015
comment
Это 12.8, пункты 31, 32.   -  person Kerrek SB    schedule 29.09.2015
comment
чтобы действительно увидеть разницу, используйте тяжелый объект для копирования. Используйте объект некоторого пользовательского типа, который имеет очень дорогостоящий конструктор копирования, например, копирование буфера изображения.   -  person basav    schedule 29.09.2015
comment
comment
@basav Я предполагаю, что std::vector, содержащий 100 000 объектов std::string, достаточно дорог для копирования.   -  person Angew is no longer proud of SO    schedule 29.09.2015


Ответы (2)


Вам не хватает фундаментальной информации: стандарт специально требует, чтобы, когда оператор return (и несколько других, менее распространенных контекстов) задавал локальную переменную функции (например, o в вашем случае), разрешение перегрузки для построения возвращаемое значение из аргумента сначала выполняется, как если бы аргумент был rvalue (даже если это не так). Только когда это не удается, снова выполняется разрешение перегрузки с lvalue. Это описано в С++ 14 12.8/32; аналогичная формулировка существует в C++11.

12.8/32 Когда выполняются критерии для исключения операции копирования/перемещения, но не для объявления-исключения, а копируемый объект обозначается lvalue , или когда выражение в операторе return является (возможно, заключенным в скобки) выражением-идентификатором, которое называет объект с автоматическим сроком хранения, объявленным в теле, или предложение-объявления-параметра< /em> самой внутренней объемлющей функции или лямбда-выражения, сначала выполняется разрешение перегрузки для выбора конструктора для копии, как если бы объект был обозначен значением r. Если первое разрешение перегрузки не выполняется или не было выполнено, или если тип первого параметра выбранного конструктора не является ссылкой rvalue на тип объекта (возможно, cv-квалифицированным), разрешение перегрузки выполняется снова, рассматривая объект как объект. значение. [ Примечание. Это двухэтапное разрешение перегрузки должно выполняться независимо от того, произойдет ли исключение копии. Он определяет конструктор, который будет вызываться, если исключение не выполняется, и выбранный конструктор должен быть доступен, даже если вызов исключен. —конец примечания ] ...

(выделено мной)

Таким образом, в каждом операторе return при возврате автоматической переменной области действия присутствует неизбежный неявный std::move.

Использование std::move в операторе return является, во всяком случае, пессимизацией. Это предотвращает NRVO и ничего не дает вам из-за правила "сначала неявно попробуйте rvalue".

person Angew is no longer proud of SO    schedule 29.09.2015
comment
@MatthiasVegh Добавлен соответствующий раздел стандарта. - person Angew is no longer proud of SO; 29.09.2015

Компилятор выполняет RVO, даже если вы указали /Od. Это разрешено стандартом С++ (§12.8/31,32, как указывает Керрек С.Б.)

Если вы действительно хотите увидеть разницу, вы можете объявить свою переменную как volatile. Это не позволит компилятору выполнять на нем RVO. (§12.8/31 пункт 1)

person anorm    schedule 29.09.2015