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

Насколько мне известно об управлении ресурсами, выделение чего-либо в кучу (оператор 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) должен закончить. Я не могу понять, почему это быстрее, чем обычное распределение.

Чем это объясняется?

19

Решение

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

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

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

Regular: 132.986
Make shared: 211.889

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

29

Другие решения

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

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

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

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

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

8

Я попробовал ваш пример в 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,

5