Зависит ли время выполнения в c ++ 11 std :: thread от полезной нагрузки - PullRequest
0 голосов
/ 02 октября 2018

Я хочу знать время, необходимое для выполнения метода в C ++ 11 std :: thread (или std :: async) по сравнению с прямым выполнением.Я знаю, что пулы потоков могут значительно сократить или даже полностью избежать этих издержек.Но я все еще хотел бы получить лучшее представление о числах.Я хотел бы знать примерно, за какую вычислительную стоимость окупается создание потока, и за какую цену окупается пул.

Я сам реализовал простой тест, который сводится к:

void PayloadFunction(double* aInnerRuntime, const size_t aNumPayloadRounds) {
    double vComputeValue = 3.14159;

    auto vInnerStart = std::chrono::high_resolution_clock::now();
    for (size_t vIdx = 0; vIdx < aNumPayloadRounds; ++vIdx) {
        vComputeValue = std::exp2(std::log1p(std::cbrt(std::sqrt(std::pow(vComputeValue, 3.14152)))));
    }
    auto vInnerEnd = std::chrono::high_resolution_clock::now();
    *aInnerRuntime += static_cast<std::chrono::duration<double, std::micro>>(vInnerEnd - vInnerStart).count();

    volatile double vResult = vComputeValue;
}

int main() {
    double vInnerRuntime = 0.0;
    double vOuterRuntime = 0.0;

    auto vStart = std::chrono::high_resolution_clock::now();
    for (size_t vIdx = 0; vIdx < 10000; ++vIdx) {
        std::thread vThread(PayloadFunction, &vInnerRuntime, cNumPayloadRounds);
        vThread.join();
    }
    auto vEnd = std::chrono::high_resolution_clock::now();
    vOuterRuntime = static_cast<std::chrono::duration<double, std::micro>>(vEnd - vStart).count();

    // normalize away the robustness iterations:
    vInnerRuntime /= static_cast<double>(cNumRobustnessIterations);
    vOuterRuntime /= static_cast<double>(cNumRobustnessIterations);

    const double vThreadCreationCost = vOuterRuntime - vInnerRuntime;
}

Это работает довольно хорошо, и я могу получить типичные затраты на создание потоков ~ 20-80 микросекунд (нас) в Ubuntu 18.04 с современным Core i7-6700K.Во-первых, это дешево по сравнению с моими ожиданиями!

Но теперь возникает любопытная часть: накладные расходы на потоки, кажется, зависят (очень воспроизводимо) от фактического времени, потраченного на метод полезной нагрузки!Это не имеет смысла для меня.Но это воспроизводимо на шести разных аппаратных машинах с различными версиями Ubuntu и CentOS!

  1. Если я потрачу от 1 до 100 мкс внутри PayloadFunction, типичная стоимость создания потока составляет около 20 долл.
  2. Когда я увеличиваю время, потраченное в PayloadFunction до 100-1000us, стоимость создания потока увеличивается примерно до 40us.
  3. Дальнейшее увеличение до более чем 10000us в PayloadFunction снова увеличивает стоимость создания потока.примерно до 80 мкс.

Я не пошел на большие диапазоны, но я ясно вижу соотношение между временем полезной нагрузки и накладными расходами потоков (как вычислено выше).Поскольку я не могу объяснить это поведение, я предполагаю, что должна быть ловушка.Возможно ли, что мои измерения времени настолько неточны?Или CPU Turbo может вызывать разные временные характеристики в зависимости от более высокой или более низкой нагрузки?Может кто-нибудь пролить свет?

Вот случайный пример времени, которое я получаю.Числа являются репрезентативными для шаблона, описанного выше.Одна и та же картина наблюдается на многих компьютерных аппаратных средствах (различные процессоры Intel и AMD) и разновидностях Linux (Ubuntu 14.04, 16.04, 18.04, CentOS 6.9 и CentOS 7.4):

payload runtime      0.3 us., thread overhead  31.3 us.
payload runtime      0.6 us., thread overhead  32.3 us.
payload runtime      2.5 us., thread overhead  18.0 us.
payload runtime      1.9 us., thread overhead  21.2 us.
payload runtime      2.5 us., thread overhead  25.6 us.
payload runtime      5.2 us., thread overhead  21.4 us.
payload runtime      8.7 us., thread overhead  16.6 us.
payload runtime     18.5 us., thread overhead  17.6 us.
payload runtime     36.1 us., thread overhead  17.7 us.
payload runtime     73.4 us., thread overhead  22.2 us.
payload runtime    134.9 us., thread overhead  19.6 us.
payload runtime    272.6 us., thread overhead  44.8 us.
payload runtime    543.4 us., thread overhead  65.9 us.
payload runtime   1045.0 us., thread overhead  70.3 us.
payload runtime   2082.2 us., thread overhead  69.9 us.
payload runtime   4160.9 us., thread overhead  76.0 us.
payload runtime   8292.5 us., thread overhead  79.2 us.
payload runtime  16523.0 us., thread overhead  86.9 us.
payload runtime  33017.6 us., thread overhead  85.3 us.
payload runtime  66242.0 us., thread overhead  76.4 us.
payload runtime 132382.4 us., thread overhead  69.1 us.

1 Ответ

0 голосов
/ 02 октября 2018

Возможно, вы выполняете какой-то код на «неправильной» стороне временных инструкций.Простой способ избежать этого - вызвать специальную инструкцию x86 CPUID.В GCC вы можете сделать это следующим образом:

#include <cpuid.h>

unsigned out[4];
__get_cpuid(1, &out[0], &out[1], &out[2], &out[3]);

Выполните такой вызов до начала синхронизации и после ее окончания.Он будет действовать как «забор», препятствующий переупорядочению операций через ваши временные границы.

...