Сравнение скорости работы range-based for, foreach(Qt) и кое-чего из STL при подсчете суммы элементов контейнеров

в 16:57, , рубрики: c++, qt, с++11, метки: ,

Я участвую в разработке проекта на 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.
Собственно, таблица результатов (меньше значение — значит быстрее):
Сравнение скорости работы range based for, foreach(Qt) и кое чего из STL при подсчете суммы элементов контейнеров

Графическое представление данных для таблицы результатов:
Сравнение скорости работы range based for, foreach(Qt) и кое чего из STL при подсчете суммы элементов контейнеров

Сравнение скорости работы range based for, foreach(Qt) и кое чего из STL при подсчете суммы элементов контейнеров

Сравнение скорости работы range based for, foreach(Qt) и кое чего из STL при подсчете суммы элементов контейнеров

Итог

Что я для себя уяснил, проведя тесты:

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

Мое мнение — нужно отказаться от использования макроса foreach в Qt проектах.

Спасибо за внимание.

P.S.: да, я знаю что можно писать ">>" без пробела.
Исходный код

Автор: c0d3r


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js