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

Почему распределение в куче быстрее, чем распределение в стеке?

Насколько мне известно об управлении ресурсами, выделение чего-то в куче (оператор new) всегда должно быть медленнее, чем выделение в стеке (автоматическое хранилище), потому что стек представляет собой структуру на основе LIFO, требует минимальной бухгалтерской отчетности, а указатель на следующий адрес для распределения является тривиальным.

До сих пор так хорошо. Теперь посмотрим на следующий код:

/* ...includes... */

using std::cout;
using std::cin;
using std::endl;

int bar() { return 42; }

int main()
{
    auto s1 = std::chrono::steady_clock::now();
    std::packaged_task<int()> pt1(bar);
    auto e1 = std::chrono::steady_clock::now();

    auto s2 = std::chrono::steady_clock::now();
    auto sh_ptr1 = std::make_shared<std::packaged_task<int()> >(bar);
    auto e2 = std::chrono::steady_clock::now();

    auto first = std::chrono::duration_cast<std::chrono::nanoseconds>(e1-s1);
    auto second = std::chrono::duration_cast<std::chrono::nanoseconds>(e2-s2);

    cout << "Regular: " << first.count() << endl
         << "Make shared: " << second.count() << endl;

    pt1();
    (*sh_ptr1)();

    cout << "As you can see, both are working correctly: " 
         << pt1.get_future().get() << " & " 
         << sh_ptr1->get_future().get() << endl;

    return 0;
}

Результаты, похоже, противоречат сказанному выше:

Обычный: 6131

Сделать общедоступным: 843

Как вы можете видеть, оба работают правильно: 42 и 42

Программа завершена кодом выхода: 0

Во втором измерении, кроме вызова оператора new, должен завершиться конструктор std::shared_ptr (auto sh_ptr1). Я не могу понять, почему это быстрее, чем регулярное выделение.

Какое объяснение для этого?

4b9b3361

Ответ 1

Проблема заключается в том, что первый вызов конструктора std::packaged_task отвечает за инициализацию нагрузки состояния в потоке, которое затем несправедливо приписывается pt1. Это обычная проблема бенчмаркинга (в частности, микробиблиотека) и смягчается путем разминки; попробуйте прочитать Как написать правильный микро-тест в Java?

Если я скопирую ваш код, но сначала запустив обе части, результаты будут такими же, что и в пределах разрешения системных часов. Это демонстрирует еще одну проблему микрообнаружения, что вы должны запускать небольшие тесты несколько раз, чтобы обеспечить точное измерение точного времени.

С разминкой и работой каждой части 1000 раз я получаю следующее (пример):

Regular: 132.986
Make shared: 211.889

Разница (около 80 нс) хорошо согласуется с эмпирическим правилом, что malloc занимает 100 нс на звонок.

Ответ 2

Это проблема с вашим микро-бенчмарком: если вы меняете порядок, в котором вы измеряете время, вы получите противоположные результаты (demo).

Похоже, что первый вызов конструктора std::packaged_task вызывает большой удар. Добавление untimed

std::packaged_task<int()> ignore(bar);

перед измерением времени устраняет эту проблему (демонстрация):

Обычный: 505
Сделать общедоступным: 937

Ответ 3

Я пробовал ваш пример на ideone и получил результат, похожий на ваш:

Regular: 67950 
Make shared: 696

Затем я отменил порядок тестов:

auto s2 = std::chrono::steady_clock::now();
auto sh_ptr1 = std::make_shared<std::packaged_task<int()> >(bar);
auto e2 = std::chrono::steady_clock::now();

auto s1 = std::chrono::steady_clock::now();
std::packaged_task<int()> pt1(bar);
auto e1 = std::chrono::steady_clock::now();

и нашел противоположный результат:

Regular: 548
Make shared: 68065

Так что не разница между стекю и кучей, но разница первого и второго вызовов. Возможно, вам нужно заглянуть внутрь std::packaged_task.