12.18 – Определение времени выполнения кода

Добавлено 10 июля 2021 в 21:27

При написании кода иногда встречаются случаи, когда вы не уверены, какой из методов будет более производительным. Так как же это узнать?

Простой способ для этого – засечь время выполнения вашего кода, чтобы узнать, сколько времени потребуется для его выполнения. В C++ 11 некоторый функционал для этого предоставляет библиотека chrono. Однако использование библиотеки chrono немного запутано. Хорошая новость заключается в том, что мы можем легко инкапсулировать весь необходимый функционал по измерению времени в класс, который затем можем использовать в своих собственных программах.

Вот этот класс:

#include <chrono> // для функций std::chrono
 
class Timer
{
private:
	// Псевдонимы типов, чтобы упростить доступ к вложенным типам
	using clock_t = std::chrono::steady_clock;
	using second_t = std::chrono::duration<double, std::ratio<1> >;
	
	std::chrono::time_point<clock_t> m_beg;
 
public:
	Timer() : m_beg(clock_t::now())
	{
	}
	
	void reset()
	{
		m_beg = clock_t::now();
	}
	
	double elapsed() const
	{
		return std::chrono::duration_cast<second_t>(clock_t::now() - m_beg).count();
	}
};

Вот и всё! Чтобы использовать его, мы создаем экземпляр объекта Timer в верхней части нашей функции main (или там, где мы хотим начать отсчет времени), а затем вызываем функцию-член elapsed() всякий раз, когда мы хотим узнать, сколько времени потребовалось программе, чтобы дойти до этой точки.

int main()
{
    Timer t;
 
    // Код, для которого измеряется время выполнения
 
    std::cout << "Time elapsed: " << t.elapsed() << " seconds\n";
 
    return 0;
}

Теперь давайте воспользуемся этим в реальном примере, где мы сортируем массив из 10000 элементов. Для начала давайте проверим алгоритм сортировки выбором, который мы реализовали в предыдущей главе:

#include <array>
#include <chrono>  // для функций std::chrono
#include <cstddef> // для std::size_t
#include <iostream>
#include <numeric> // для std::iota
 
const int g_arrayElements = 10000;
 
class Timer
{
private:
  // Псевдонимы типов, чтобы упростить доступ к вложенным типам
  using clock_t = std::chrono::steady_clock;
  using second_t = std::chrono::duration<double, std::ratio<1>>;
 
  std::chrono::time_point<clock_t> m_beg;
 
public:
  Timer() : m_beg(clock_t::now())
  {
  }
 
  void reset()
  {
    m_beg = clock_t::now();
  }
 
  double elapsed() const
  {
    return std::chrono::duration_cast<second_t>(clock_t::now() - m_beg).count();
  }
};
 
void sortArray(std::array<int, g_arrayElements>& array)
{
 
  // Пройдемся по каждому элементу массива
  // (кроме последнего, который уже будет отсортирован
  // к тому моменту, когда мы до него доберемся)
  for (std::size_t startIndex{ 0 }; startIndex < (g_arrayElements - 1); ++startIndex)
  {
    // smallestIndex - это индекс наименьшего элемента, с которым мы столкнулись в этой итерации
    // Начнем с предположения, что наименьший элемент является первым элементом этой итерации
    std::size_t smallestIndex{ startIndex };
 
    // Затем ищем меньший элемент в остальной части массива
    for (std::size_t currentIndex{ startIndex + 1 }; currentIndex < g_arrayElements; ++currentIndex)
    {
      // Если мы нашли элемент, который меньше нашего ранее найденного наименьшего
      if (array[currentIndex] < array[smallestIndex])
      {
        // тогда отслеживаем его
        smallestIndex = currentIndex;
      }
    }
 
    // smallestIndex теперь самый маленький элемент в оставшемся массиве
    // меняем местами наш начальный элемент самым маленьким элементом
    // (это сортирует его в нужное место)
    std::swap(array[startIndex], array[smallestIndex]);
  }
}
 
int main()
{
  std::array<int, g_arrayElements> array;
  // заполняем массив от 10000 до 1
  std::iota(array.rbegin(), array.rend(), 1);
 
  Timer t;
 
  sortArray(array);
 
  std::cout << "Time taken: " << t.elapsed() << " seconds\n";
 
  return 0;
}

На машине автора три прогона дали значения времени выполнения 0,0507, 0,0506 и 0,0498. Таким образом, мы можем сказать, что время выполнения сортировки составляет около 0,05 секунды.

Теперь давайте проделаем тот же тест, используя std::sort из стандартной библиотеки.

#include <algorithm> // для std::sort
#include <array>
#include <chrono>    // для функций std::chrono functions
#include <cstddef>   // для std::size_t
#include <iostream>
#include <numeric>   // для std::iota
 
const int g_arrayElements = 10000;
 
class Timer
{
private:
  // Псевдонимы типов, чтобы упростить доступ к вложенным типам
  using clock_t = std::chrono::steady_clock;
  using second_t = std::chrono::duration<double, std::ratio<1>>;
 
  std::chrono::time_point<clock_t> m_beg;
 
public:
  Timer() : m_beg(clock_t::now())
  {
  }
 
  void reset()
  {
    m_beg = clock_t::now();
  }
 
  double elapsed() const
  {
    return std::chrono::duration_cast<second_t>(clock_t::now() - m_beg).count();
  }
};
 
int main()
{
  std::array<int, g_arrayElements> array;
  // заполняем массив от 10000 до 1
  std::iota(array.rbegin(), array.rend(), 1);
 
  Timer t;
 
  std::ranges::sort(array); // Начиная с C++20
  // Если ваш компилятор не поддерживает C++20
  // std::sort(array.begin(), array.end());
 
  std::cout << "Time taken: " << t.elapsed() << " seconds\n";
 
  return 0;
}

На машине автора эта программа дала результаты: 0,000693, 0,000692 и 0,000699. То есть около 0,0007.

Другими словами, в этом случае std::sort в 100 раз быстрее, чем сортировка выбором, которую мы написали сами!

Несколько предостережений о засекании времени выполнения

Засекание времени выполнения – простой метод, но на ваши результаты может значительно повлиять ряд факторов, и о них важно знать.

Во-первых, убедитесь, что вы используете конфигурацию сборки выпуска, а не конфигурацию отладочной сборки. Конфигурации отладочной сборки обычно отключают оптимизацию, а эта оптимизация может существенно повлиять на результаты. Например, при использовании конфигурации отладочной сборки выполнение приведенного выше примера с std::sort на машине автора заняло 0,0235 секунды – в 33 раза дольше!

Во-вторых, на ваши результаты измерения времени будут влиять другие вещи, которые ваша система может делать в фоновом режиме. Для достижения наилучших результатов убедитесь, что ваша система не выполняет никаких операций с интенсивным использованием CPU или памяти (например, игры) или жесткого диска (например, поиск файла или запуск сканирования антивирусом).

Засекайте время выполнения не менее 3-х раз. Если все результаты похожи, возьмите среднее. Если один или два результата сильно отличаются, запустите программу еще несколько раз, пока не получите лучшее представление о том, какие из результатов являются выбросами. Обратите внимание, что кажущиеся невинными вещи, такие как веб-браузеры, могут временно увеличить загрузку вашего процессора до 100%, когда сайт, который открыт в фоновом режиме, запускает новый рекламный баннер и должен проанализировать кучу кода JavaScript. Многократный запуск помогает определить, могло ли такое событие повлиять на ваш первоначальный запуск.

В-третьих, при сравнении двух фрагментов кода будьте осторожны с тем, что может измениться между запусками, что может повлиять на измеренное время. Возможно, ваша система запустила сканирование антивирусом в фоновом режиме, или, может быть, вы сейчас стримите музыку, тогда как раньше этого не делали. Рандомизация также может повлиять на измеренное время. Если бы мы сортировали массив, заполненный случайными числами, на результаты могла бы повлиять рандомизация. Рандомизацию по-прежнему можно использовать, но убедитесь, что вы используете фиксированное начальное число (например, не используете системные часы), чтобы рандомизация была одинаковой при каждом запуске. Кроме того, убедитесь, что вы не ожидаете ввода данных пользователем во время измерения времени выполнения, поскольку время, необходимое пользователю для ввода чего-либо, не должно быть частью ваших измерений времени.

Наконец, обратите внимание, что результаты действительны только для архитектуры вашего компьютера, ОС, компилятора и системных характеристик. Вы можете получить отличающиеся результаты на других системах, которые имеют другие сильные и слабые стороны.

Теги

C++ / CppchronoLearnCppДля начинающихОбучениеПрограммирование

На сайте работает сервис комментирования DISQUS, который позволяет вам оставлять комментарии на множестве сайтов, имея лишь один аккаунт на Disqus.com.

В случае комментирования в качестве гостя (без регистрации на disqus.com) для публикации комментария требуется время на премодерацию.