Вчера на работе мой коллега заявил, что макросы препроцессора работают медленнее, чем запись переменных и функций вручную. Контекст таков, что у нас есть класс, в который иногда добавляются переменные-члены, и для каждой из этих переменных-членов необходимо создать 3 разных метода по одному и тому же шаблону. Мы создали их автоматически с помощью макросов, как показано ниже.
struct Bar
{
long long a;
long long b;
long long c;
long long d;
};
struct Foo
{
Bar var[1300];
};
typedef std::vector<Foo> TEST_TYPE ;
class A
{
private:
TEST_TYPE container;
public:
TEST_TYPE& getcontainer()
{
return container;
}
};
#define createBMember(TYPE, NAME) \
private: \
TYPE NAME; \
\
public: \
TYPE& get##NAME() \
{ \
return NAME; \
}
class B
{
createBMember(TEST_TYPE, container);
};
double testA()
{
A a;
LARGE_INTEGER frequency;
LARGE_INTEGER startA, endA;
if (!QueryPerformanceFrequency(&frequency)) {
std::cerr << "High-Resolution-Timer nicht unterstützt." << std::endl;
return 1;
}
QueryPerformanceCounter(&startA);
for(size_t i = 0; i < 10000; ++i)
{
a.getcontainer().push_back(Foo());
}
QueryPerformanceCounter(&endA);
return static_cast<double>(endA.QuadPart - startA.QuadPart) / frequency.QuadPart;
}
double testB()
{
B b;
LARGE_INTEGER frequency;
LARGE_INTEGER startB, endB;
if (!QueryPerformanceFrequency(&frequency)) {
std::cerr << "High-Resolution-Timer nicht unterstützt." << std::endl;
}
QueryPerformanceCounter(&startB);
for(size_t i = 0; i < 10000; ++i)
{
b.getcontainer().push_back(Foo());
}
QueryPerformanceCounter(&endB);
return static_cast<double>(endB.QuadPart - startB.QuadPart) / frequency.QuadPart;
}
//----------------------------------------------------[main]
int main()
{
double Atest = 0;
double Btest = 0;
double AHigh = 0;
double BHigh = 0;
double ALow = 10000;
double BLow = 10000;
double a;
double b;
const uint16_t amount = 30;
for(uint16_t i = 0; i < amount; ++i)
{
a = testA();
AHigh = a > AHigh ? a : AHigh;
ALow = a < ALow ? a : ALow;
Atest += a;
}
for(uint8_t i = 0; i < amount; ++i)
{
b = testB();
BHigh = b > BHigh ? b : BHigh;
BLow = b < BLow ? b : BLow;
Btest += b;
}
Atest /= amount;
Btest /= amount;
std::cout << "A: " << Atest << std::endl;
std::cout << "B: " << Btest << std::endl;
auto size = sizeof(Foo);
return 0;
}
В этом тесте я попытался опровергнуть его утверждение, имея довольно большую структуру, которую я просто добавляю в вектор при каждом запуске теста.
Однако странность заключалась в том, что, хотя препроцессор запускается до компиляции и поэтому оба класса должны быть идентичными, я измерил некоторые различия в скорости. Были сделаны следующие наблюдения:
Это меня смущает, потому что, как я уже сказал, оба класса идентичны.
Я также должен упомянуть, что, к сожалению, что касается нашей среды разработки, нам приходится работать со своего рода моментальной версией C++11 (Visual Studio 2010). Вот почему я не могу использовать std::chrono, например, для сравнительного анализа.
Мне еще не удалось протестировать его с другими компиляторами. Я также просмотрел ассемблерный код на godbolt.org, но не нашел ничего, что могло бы иметь такое большое значение.
Признаюсь, я все еще стажер и отношу свои навыки скорее к любительским. Кто-нибудь знает, что может быть причиной такой разницы в скорости?
Я еще не проверял это. Но результат такой: при тестировании я сделал по 30 прогонов в релизной конфигурации. TestA работает быстрее с классами A и B. Затем я поменял местами тест, который выполняется первым, т. е. поменял местами циклы for. В этом случае TestA все равно работает быстрее в обоих случаях. Теперь я снова поменял местами две функции и протестировал их. При тестировании с классом A TestB работает быстрее. Однако при тестировании с классом B TestA снова оказывается быстрее.
Вы пробовали поменять порядок тестов. Первый запуск может замедлиться из-за того, что память в последнее время не использовалась. Протестируйте A, B, A, B, чтобы убедиться, что память, выделяемая в первый раз, не искажает ваши тесты. Я не понимаю, как исходный код, сгенерированный препроцессором, должен отличаться при компиляции. Я уверен, что это добавит что-то ко времени компиляции, но вообще не должно влиять на время выполнения.
Да, я тоже это проверял. В общем, первый тест обычно выполняется быстрее, а не медленнее.
Векторы будут выполнять множество операций по распределению кучи, и, конечно же, при первом запуске состояние кучи будет другим. Итак, вы тестируете кучу или макросы? :-)
Вы можете запустить препроцессор самостоятельно, используя cl /P
в файле. Это поможет вам увидеть, есть ли какие-либо различия, которые вы, возможно, не учли, и, безусловно, должно включать в себя возможность того, что наличие макросов каким-либо образом влияет на скорость конечной программы. Теоретически компилятор C++ может попытаться что-то сделать с макросами в файле, который не был предварительно обработан, но я был бы очень удивлен, если бы Visual C++ (или любой другой компилятор C++) это сделал.
Я имею в виду, (endB.QuadPart - startB.QuadPart) / frequency.QuadPart
это ерунда
testA
и testB
должны быть шаблонами типа T, чтобы уменьшить вероятность опечаток. a.getcontainer().push_back(Foo());
, наверное, должно быть emplace_back()
.
Макросы — это текстовая замена в препроцессоре, один из самых ранних этапов компиляции. Фактический компилятор видит идентичный код. Любая разница в скорости будет обусловлена другими факторами. Как отмечается в комментариях, плохая методология тестирования почти наверняка является лучшим объяснением (особенно с учетом отсутствия знаний, показанного как в заявлении, так и в использовании VS 2010).
Я согласен. Я также озадачен, почему они застряли на такой устаревшей версии компилятора.
Вы пытались использовать один и тот же класс в обоих тестах?