Честно говоря, написать программу для сравнения производительности - тривиально:
#include <ctime>
#include <iostream>
namespace {
class empty { }; // even empty classes take up 1 byte of space, minimum
}
int main()
{
std::clock_t start = std::clock();
for (int i = 0; i < 100000; ++i)
empty e;
std::clock_t duration = std::clock() - start;
std::cout << "stack allocation took " << duration << " clock ticks\n";
start = std::clock();
for (int i = 0; i < 100000; ++i) {
empty* e = new empty;
delete e;
};
duration = std::clock() - start;
std::cout << "heap allocation took " << duration << " clock ticks\n";
}
Говорят, что глупая последовательность - это хобгоблин маленьких умов . По-видимому, оптимизирующие компиляторы являются предметом внимания многих программистов. Эта дискуссия была в нижней части ответа, но люди, видимо, не удосужились прочитать так далеко, поэтому я перенесу это сюда, чтобы избежать вопросов, на которые я уже ответил.
Оптимизирующий компилятор может заметить, что этот код ничего не делает, и может все это оптимизировать. Работа оптимизатора заключается в том, чтобы делать подобные вещи, и сражаться с оптимизатором - глупое дело.
Я бы порекомендовал компилировать этот код с отключенной оптимизацией, потому что нет хорошего способа обмануть каждый оптимизатор, который используется в настоящее время или будет использоваться в будущем.
Любой, кто включает оптимизатор, а затем жалуется на борьбу с ним, должен подвергаться публичным насмешкам.
Если бы я заботился о точности наносекунды, я бы не использовал std::clock()
. Если бы я хотел опубликовать результаты в качестве докторской диссертации, я бы об этом подумал побольше и, вероятно, сравнил бы GCC, Tendra / Ten15, LLVM, Watcom, Borland, Visual C ++, Digital Mars, ICC и другие компиляторы. На самом деле, выделение кучи занимает в сотни раз больше времени, чем выделение стека, и я не вижу ничего полезного в дальнейшем рассмотрении вопроса.
У оптимизатора есть миссия избавиться от кода, который я тестирую. Я не вижу причин говорить оптимизатору, чтобы он запускался, а затем пытался обмануть оптимизатор, чтобы он не оптимизировал. Но если бы я увидел ценность в этом, я бы сделал одно или несколько из следующих действий:
Добавить элемент данных в empty
и получить доступ к этому элементу данных в цикле; но если я только когда-либо прочитал данные, член оптимизатора может сделать постоянное свертывание и удалить цикл; если я только когда-либо напишу в элемент данных, оптимизатор может пропустить все, кроме самой последней итерации цикла. Кроме того, вопрос заключался не в «распределении стека и доступе к данным в сравнении с распределением кучи и доступом к данным».
Объявить e
volatile
, , но volatile
часто неправильно компилируется (PDF).
Возьмите адрес e
внутри цикла (и, возможно, присвойте его переменной, которая объявлена extern
и определена в другом файле). Но даже в этом случае компилятор может заметить, что - по крайней мере, в стеке - e
всегда будет выделяться по одному и тому же адресу памяти, а затем выполнять постоянное свертывание, как в (1) выше. Я получаю все итерации цикла, но объект никогда не выделяется.
Помимо очевидного, этот тест имеет недостатки в том, что он измеряет как распределение, так и освобождение, а первоначальный вопрос не касался освобождения. Конечно, переменные, расположенные в стеке, автоматически освобождаются в конце их области, поэтому не вызов delete
приведет к (1) искажению чисел (освобождение стека включено в числа о выделении стека, поэтому справедливо измерить освобождение кучи) ) и (2) вызывают довольно серьезную утечку памяти, если только мы не сохраним ссылку на новый указатель и не вызовем delete
после того, как у нас будет измерение времени.
На моей машине, используя g ++ 3.4.4 в Windows, я получаю «0 тактов» как для размещения в стеке, так и в куче для всего, что меньше 100000 выделений, и даже тогда я получаю «0 тактов» для распределения в стеке и « 15 тактов "для выделения кучи. Когда я измеряю 10 000 000 выделений, выделение стека занимает 31 такт, а выделение кучи - 1562 такта.
Да, оптимизирующий компилятор может исключить создание пустых объектов. Если я правильно понимаю, это может даже исключить весь первый цикл. Когда я увеличил число итераций до 10 000 000, выделение стека заняло 31 такт, а выделение кучи - 1562 такта. Я думаю, можно с уверенностью сказать, что, не сказав g ++ оптимизировать исполняемый файл, g ++ не исключил конструкторов.
За годы, прошедшие с того момента, как я это написал, в Stack Overflow предпочтение отдавалось повышению производительности за счет оптимизированных сборок. В общем, я думаю, что это правильно. Тем не менее, я все еще думаю, что глупо просить компилятор оптимизировать код, когда вы на самом деле не хотите, чтобы этот код был оптимизирован. Мне кажется, что я очень похож на то, чтобы доплачивать за парковку, но отказываюсь сдавать ключи. В данном конкретном случае я не хочу, чтобы оптимизатор работал.
Использование слегка модифицированной версии эталонного теста (для решения правильной точки, в которой исходная программа не выделяла что-либо в стеке каждый раз в цикле) и компиляция без оптимизации, но с привязкой к библиотекам релиза (для решения действительной точки что мы не хотим включать какое-либо замедление, вызванное ссылками на библиотеки отладки):
#include <cstdio>
#include <chrono>
namespace {
void on_stack()
{
int i;
}
void on_heap()
{
int* i = new int;
delete i;
}
}
int main()
{
auto begin = std::chrono::system_clock::now();
for (int i = 0; i < 1000000000; ++i)
on_stack();
auto end = std::chrono::system_clock::now();
std::printf("on_stack took %f seconds\n", std::chrono::duration<double>(end - begin).count());
begin = std::chrono::system_clock::now();
for (int i = 0; i < 1000000000; ++i)
on_heap();
end = std::chrono::system_clock::now();
std::printf("on_heap took %f seconds\n", std::chrono::duration<double>(end - begin).count());
return 0;
}
отображает:
on_stack took 2.070003 seconds
on_heap took 57.980081 seconds
в моей системе при компиляции с командной строкой cl foo.cc /Od /MT /EHsc
.
Вы можете не согласиться с моим подходом к получению неоптимизированной сборки. Это нормально: не стесняйтесь изменять эталонный тест столько раз, сколько хотите. Когда я включаю оптимизацию, я получаю:
on_stack took 0.000000 seconds
on_heap took 51.608723 seconds
Не потому, что выделение стека на самом деле происходит мгновенно, а потому, что любой полуприличный компилятор может заметить, что on_stack
не делает ничего полезного и может быть оптимизировано. GCC на моем ноутбуке с Linux также замечает, что on_heap
не делает ничего полезного, а также оптимизирует его:
on_stack took 0.000003 seconds
on_heap took 0.000002 seconds