Я участвую в разработке проекта на C++ с использованием фреймворка Qt. В нашем проекте во многих местах используются контейнеры Qt и для обхода элементов часто применяется макрос foreach. В один прекрасный момент мне стало интересно, насколько оправдано применение этого макроса. Кроме того, очень хотелось «пощупать» c++11 в действии. И вот что мне удалось на текущий момент выяснить...
Написание вспомогательных функций
Контейнеры в 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.: да, я знаю что можно писать ">>" без пробела.
Исходный код
Автор: c0d3r