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

Почему при использовании std:: async выполняется переключение const ref.

В качестве упражнения, чтобы узнать о std::async, я написал небольшую программу, которая вычисляет сумму большого vector<int>, распределенного вокруг большого количества потоков.

Мой следующий код выглядит следующим образом

#include <iostream>
#include <vector>
#include <future>
#include <chrono>

typedef unsigned long long int myint;

// Calculate sum of part of the elements in a vector
myint partialSum(const std::vector<myint>& v, int start, int end)
{
    myint sum(0);
    for(int i=start; i<=end; ++i)
    {
        sum += v[i];
    }
    return sum;
}

int main()
{
    const int nThreads = 100;
    const int sizePerThread = 100000;
    const int vectorSize = nThreads * sizePerThread;

    std::vector<myint> v(vectorSize);   
    std::vector<std::future<myint>> partial(nThreads);
    myint tot = 0;

    // Fill vector  
    for(int i=0; i<vectorSize; ++i)
    {
        v[i] = i+1;
    }
    std::chrono::steady_clock::time_point startTime = std::chrono::steady_clock::now();

    // Start threads
    for( int t=0; t < nThreads; ++t)
    {
        partial[t] = std::async( std::launch::async, partialSum, v, t*sizePerThread, (t+1)*sizePerThread -1);               
    }

    // Sum total
    for( int t=0; t < nThreads; ++t)
    {
        myint ps = partial[t].get();
        std::cout << t << ":\t" << ps << std::endl;
        tot += ps;
    }
    std::cout << "Sum:\t" << tot << std::endl;

    std::chrono::steady_clock::time_point endTime = std::chrono::steady_clock::now();
    std::cout << "Time difference = " << std::chrono::duration_cast<std::chrono::milliseconds>(endTime - startTime).count() <<std::endl;
}

Мой вопрос касается вызовов функции partialSum, а затем, в особенности, как передается большой вектор. Функция вызывается следующим образом:

        partial[t] = std::async( std::launch::async, partialSum, v, t*sizePerThread, (t+1)*sizePerThread -1);       

с определением следующим образом

myint partialSum(const std::vector<myint>& v, int start, int end)

При таком подходе расчет относительно медленный. Если я использую std::ref(v) в вызове функции std::async, моя функция намного быстрее и эффективнее. Это все еще имеет смысл для меня.

Однако, если я еще вызываю v вместо std::ref(v), но заменяю функцию

myint partialSum(std::vector<myint> v, int start, int end)

программа также работает намного быстрее (и использует меньше памяти). Я не понимаю, почему реализация const ref медленнее. Как компилятор исправляет это без каких-либо ссылок?

При реализации const ref эта программа обычно занимает 6,2 секунды для запуска, без 3.0. (Обратите внимание, что с константой ref и std::ref для меня это работает на 0,2 секунды)

Я компилирую с помощью g++ -Wall -pedantic, используя (добавление -O3 при передаче только v демонстрирует тот же эффект)

g++ --version

g++ (Rev1, построенный по проекту MSYS2). 6.3.0 Copyright (C) 2016 Free Software Foundation, Inc. Это бесплатное программное обеспечение; см. источник условий копирования. Здесь нет гарантия; даже для КОММЕРЧЕСКОЙ ЦЕННОСТИ или ПРИГОДНОСТИ ДЛЯ ОПРЕДЕЛЕННОЙ ЦЕЛИ.

4b9b3361

Ответ 1

Рассказ

с учетом типа копирования и перемещения типа T

V f(T);
V g(T const&);

T t;
auto v = std::async(f,t).get();
auto v = std::async(g,t).get();

единственное релевантное различие, касающееся двух асинхронных вызовов, заключается в том, что в первом случае копия t уничтожается, как только f возвращается; во второй, t-копия может быть уничтожена в соответствии с эффектом вызова get(). Если асинхронные вызовы происходят в цикле с будущим get() позже, первое будет иметь постоянную память на avarage (при условии постоянной рабочей нагрузки на потоке), а вторая - линейно растущая память в худшем случае, что приводит к большему количеству просмотров кеша и хуже производительность распределения.

Длинная история

Прежде всего, я могу воспроизвести наблюдаемое замедление (последовательно ~ 2x в моей системе) как на gcc, так и на clang; кроме того, тот же код с эквивалентными std::thread invocations не проявляет того же поведения, при этом const & версия получается немного быстрее, как ожидалось. Посмотрим, почему.

Во-первых, спецификация async читает:

[futures.async] Если запуск:: async задан в политике, вызывает INVOKE (DECAY_COPY (std:: forward (f)), DECAY_COPY (std:: forward (args))...) (23.14.3, 33.3.2.2), как будто в новом потоке выполнения, представленном объектом потока, с вызовами DECAY_COPY(), которые вычисляются в потоке, который вызвал async [...] Тема объект хранится в общем состоянии и влияет на поведение любых асинхронных возвращаемых объектов, которые ссылаются на это состояние.

поэтому async скопирует аргументы, пересылающие эти копии вызываемому, сохраняя rvalueness; в этом отношении он аналогичен конструктору std::thread, и нет разницы в двух версиях OP, скопируйте вектор.

Разница заключается в полужирной части: объект потока является частью общего состояния и не будет освобожден до тех пор, пока последний не будет освобожден (например, вызовом future:: get()).

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

Фактически, оказывается, что реализации gcc и clang хранят разложившиеся копии в общем состоянии будущего.

Следовательно, в const & версия векторная копия хранится в общем состоянии и уничтожается в future::get: , это приводит к тому, что цикл "Начать потоки" выделяет новый вектор на каждом шаге с линейным ростом памяти.. p >

И наоборот, в версии по значению векторная копия перемещается в вызываемом аргументе и уничтожается, как только вызываемый возвращается; в future::get, перемещенный пустой вектор будет уничтожен. Таким образом, если вызываемый достаточно быстро уничтожает вектор до создания нового, один и тот же вектор будет передаваться снова и снова, а память останется почти постоянной. Это приведет к уменьшению количества кешей и более быстрым распределениям, объясняя улучшенные тайминги.

Ответ 2

Как говорили люди, без std:: ref объект копируется.

Теперь я считаю, что причина, по которой передача по значению на самом деле быстрее, может иметь какое-то отношение к следующему вопросу: Лучше ли в С++ передавать по значению или передавать по константной ссылке?

Что может случиться, что во внутренней реализации async вектор копируется один раз в новый поток. И затем внутренне передается по ссылке, к функции, которая берет на себя ответственность за вектор, что означает, что он будет скопирован еще раз. С другой стороны, если вы передадите его по значению, он будет копировать его один раз в новый поток, но будет перемещать его дважды в новый поток. Результат в 2-х экземплярах, если объект передается по ссылке, а 1 копия и 2 перемещаются во втором случае, если объект передается по значению.