Я участвую в разработке проекта на C++ с использованием фреймворка Qt. В нашем проекте во многих местах используются контейнеры Qt и для обхода элементов часто применяется макрос foreach. В один прекрасный момент мне стало интересно, насколько оправдано применение этого макроса. Кроме того, очень хотелось «пощупать» c++11 в действии. И вот что мне удалось на текущий момент выяснить...
Контейнеры в STL и Qt не имеют общего базового класса, поэтому была предпринята попытка обойтись шаблонными функциями, чтобы не писать функцию тестирования для каждого контейнера, благо Qt контейнеры поддерживают STL-style итераторы.
На текущий момент моя основная функция для тестирования выглядит вот так:
Параметр testCycles задает количество итераций одного теста. Шаблонная функция makeTestClass конструирует класс для тестирования. Для векторов она выглядит так:
Ну а для QList что-то типа такого:
TestProcs::getRegisteredTests возвращает вектор из названия теста и процедуры для выполнения теста над контейнером.
makeTestResults собственно и производит запуск теста заданное количество раз и возвращает вектор измеренного времени выполнения каждой итерации. По этим данным считается среднее, минимальное и максимальное значение времени выполнения итерации теста.
Список контейнеров, подвергшихся тестированию:
Количество итераций для каждого теста = 20.
Размер данных контейнера равен 30 мегабайт.
Время измерялось c помощью разности значений rdtsc, деленной
на 100000.
Собственно, таблица результатов (меньше значение — значит быстрее):

Графическое представление данных для таблицы результатов:



Что я для себя уяснил, проведя тесты:
Мое мнение — нужно отказаться от использования макроса foreach в Qt проектах.
Спасибо за внимание.
P.S.: да, я знаю что можно писать ">>" без пробела.
Исходный код
Написание вспомогательных функций
Контейнеры в STL и Qt не имеют общего базового класса, поэтому была предпринята попытка обойтись шаблонными функциями, чтобы не писать функцию тестирования для каждого контейнера, благо Qt контейнеры поддерживают STL-style итераторы.
На текущий момент моя основная функция для тестирования выглядит вот так:
template<typename TEST_CLASS> void runTests(int testCycles) { auto container = makeTestClass<TEST_CLASS>(); using namespace TestProcs; static auto tests = TestProcs::getRegisteredTests<TEST_CLASS>(); std::cout << "Run test for type:" << typeid(TEST_CLASS()).name() << std::endl; for (auto test : tests) { auto warmResultsTest = makeTestResults(*container, test.second, testCycles, true); std::cout << "\"" << test.first << "\" results(ms):" << getResultsString(warmResultsTest) << std::endl; } }
Параметр testCycles задает количество итераций одного теста. Шаблонная функция makeTestClass конструирует класс для тестирования. Для векторов она выглядит так:
template<typename TEST_CLASS> std::shared_ptr<TEST_CLASS> makeTestClass() { return std::shared_ptr < TEST_CLASS > (new TEST_CLASS(VECTOR_SIZE, 0)); }
Ну а для QList что-то типа такого:
template<> std::shared_ptr<QList<TestType> > makeTestClass() { return std::shared_ptr < QList<TestType> > (new QList<TestType>( QList<TestType>::fromVector(QVector<TestType>(VECTOR_SIZE)))); }
TestProcs::getRegisteredTests возвращает вектор из названия теста и процедуры для выполнения теста над контейнером.
makeTestResults собственно и производит запуск теста заданное количество раз и возвращает вектор измеренного времени выполнения каждой итерации. По этим данным считается среднее, минимальное и максимальное значение времени выполнения итерации теста.
Список тестов, которые я проводил
- std::accumulate (doAccumulateTest)
- foreach макрос Qt (doQtForeachTest)
- range-based for (doNewForTestt)
- STL-style for (doSTLForTest)
- STL-style for с вычислением конца контейнера вне цикла (doSTLForTest2)
namespace TestProcs { template<typename CONTAINER> int doAccumulateTest(CONTAINER& container) { typename CONTAINER::value_type sum = typename CONTAINER::value_type(); sum = std::accumulate(container.begin(), container.end(), sum); return sum; } template<typename CONTAINER> int doQtForeachTest(CONTAINER& container) { typename CONTAINER::value_type sum = typename CONTAINER::value_type(); foreach(auto item, container) { sum += item; } return sum; } template<typename CONTAINER> int doNewForTest(CONTAINER& container) { typename CONTAINER::value_type sum = typename CONTAINER::value_type(); for (auto item : container) { sum += item; } return sum; } template<typename CONTAINER> int doSTLForTest(CONTAINER& container) { typename CONTAINER::value_type sum = typename CONTAINER::value_type(); auto it = container.begin(); for (; it != container.end(); ++it) { sum += *it; } return sum; } template<typename CONTAINER> int doSTLForTest2(CONTAINER& container) { typename CONTAINER::value_type sum = typename CONTAINER::value_type(); auto it = container.begin(); auto end = container.end(); for (; it != end; ++it) { sum += *it; } return sum; } template<typename TEST_CLASS> const std::vector<typename Typedefs<TEST_CLASS>::TestProcRecord>& getRegisteredTests() { static const std::vector<typename Typedefs<TEST_CLASS>::TestProcRecord> tests { { "New for", &doNewForTest<TEST_CLASS> }, { "Accumulate", &doAccumulateTest<TEST_CLASS> }, { "Qt foreach", &doQtForeachTest<TEST_CLASS> }, { "STL for", &doSTLForTest<TEST_CLASS> }, { "STL for 2", &doSTLForTest2<TEST_CLASS> } }; return tests; } }
Результаты тестирования
Список контейнеров, подвергшихся тестированию:
- QVector<qint32> (runTests<QVector<TestType> >(TEST_CYCLES))
- std::vector<qint32> (runTests<std::vector<TestType> >(TEST_CYCLES))
- QList<qint32> (runTests<QList<TestType> >(TEST_CYCLES))
Количество итераций для каждого теста = 20.
Размер данных контейнера равен 30 мегабайт.
Время измерялось c помощью разности значений rdtsc, деленной
на 100000.
Собственно, таблица результатов (меньше значение — значит быстрее):

Графическое представление данных для таблицы результатов:



Итог
Что я для себя уяснил, проведя тесты:
- Макрос foreach почти всегда медленнее «правильных» STL-циклов (таких, где конечное значение итератора вычисляется перед началом цикла)
- Макрос foreach категорически противопоказан к использованию с STL-контейнерами, т.к. делает глубокую копию контейнеров (в Qt контейнерах используется COW, поэтому они не подвержены катастрофическому «проседанию» производительности)
- Для std::vector неважно где вычисляется конечное значение итератора (перед началом цикла или «внутри» тела)
Мое мнение — нужно отказаться от использования макроса foreach в Qt проектах.
Спасибо за внимание.
P.S.: да, я знаю что можно писать ">>" без пробела.
Исходный код