Ответ 1
Мне было интересно, можно ли улучшить точность преобразования, предложенного ТС и Говардом Хиннантом. Для справки, вот базовая версия, которую я тестировал.
template
<
typename DstTimePointT,
typename SrcTimePointT,
typename DstClockT = typename DstTimePointT::clock,
typename SrcClockT = typename SrcTimePointT::clock
>
DstTimePointT
clock_cast_0th(const SrcTimePointT tp)
{
const auto src_now = SrcClockT::now();
const auto dst_now = DstClockT::now();
return dst_now + (tp - src_now);
}
Используя тест
int
main()
{
using namespace std::chrono;
const auto now = system_clock::now();
const auto steady_now = CLOCK_CAST<steady_clock::time_point>(now);
const auto system_now = CLOCK_CAST<system_clock::time_point>(steady_now);
const auto diff = system_now - now;
std::cout << duration_cast<nanoseconds>(diff).count() << '\n';
}
где CLOCK_CAST
будет #define
d, а пока, clock_cast_0th
, я собрал гистограмму для незанятой системы и для одной под высокой нагрузкой. Обратите внимание, что это тест холодного запуска. Сначала я попытался вызвать функцию в цикле, где она дает гораздо лучшие результаты. Тем не менее, я думаю, что это создало бы ложное впечатление, потому что большинство реальных программ, вероятно, время от времени преобразуют момент времени и попадут в холодный случай.
Нагрузка была сгенерирована путем запуска следующих задач параллельно с тестовой программой. (Мой компьютер имеет четыре процессора.)
- Тест умножения матриц (однопоточный).
-
find/usr/include -execdir grep "$(pwgen 10 1)" '{}' \; -print
-
hexdump/dev/urandom | gzip | hexdump | gzip | hexdump | gzip | hexdump | gzip | hexdump | gzip | hexdump | gzip | hexdump | gzip | hexdump | gzip | hexdump | gzip | hexdump | gzip| gunzip >/dev/null
-
dd if=/dev/urandom of=/tmp/spam bs=10 count=1000
Те команды, которые заканчивались за конечное время, выполнялись в бесконечном цикле.
Следующая гистограмма, а также последующие гистограммы показывают ошибки 50 000 прогонов с удалением наихудшего 1..
Обратите внимание, что ордината имеет логарифмическую шкалу.
Ошибки примерно попадают в диапазон от 0,5 мкс до 1,0 мкс в холостом случае и от 0,5 мкс до 1,5 мкс в предполагаемом случае.
Самое поразительное наблюдение заключается в том, что распределение ошибок далеко не симметрично (отрицательных ошибок нет вообще), что указывает на большую систематическую составляющую ошибки. Это имеет смысл, потому что если мы прерываемся между двумя вызовами now
, ошибка всегда в одном и том же направлении, и мы не можем быть прерваны на "отрицательное количество времени".
Гистограмма для рассматриваемого случая почти выглядит как идеальное экспоненциальное распределение (следите за логарифмической шкалой!) С довольно резким срезом, который кажется правдоподобным; вероятность того, что вас прервут на время t, примерно пропорциональна e - t.
Затем я попытался использовать следующий трюк
template
<
typename DstTimePointT,
typename SrcTimePointT,
typename DstClockT = typename DstTimePointT::clock,
typename SrcClockT = typename SrcTimePointT::clock
>
DstTimePointT
clock_cast_1st(const SrcTimePointT tp)
{
const auto src_before = SrcClockT::now();
const auto dst_now = DstClockT::now();
const auto src_after = SrcClockT::now();
const auto src_diff = src_after - src_before;
const auto src_now = src_before + src_diff / 2;
return dst_now + (tp - src_now);
}
надеясь, что интерполяция scr_now
частично отменит ошибку, scr_now
неизбежным вызовом часов в последовательном порядке.
В первой версии этого ответа я утверждал, что это ничего не помогло. Оказывается, это не было правдой. После того, как Говард Хиннант указал, что он наблюдал улучшения, я улучшил свои тесты, и теперь наблюдается некоторое заметное улучшение.
Это не было большим улучшением с точки зрения диапазона ошибок, однако, ошибки теперь примерно центрированы вокруг нуля, что означает, что у нас теперь есть ошибки в диапазоне от -0,5 & # 1202f; мкс до 0,5 & # 1202f; мкс., Более симметричное распределение указывает на то, что статистическая составляющая ошибки стала более доминирующей.
Затем я попытался вызвать вышеуказанный код в цикле, который выбрал бы лучшее значение для src_diff
.
template
<
typename DstTimePointT,
typename SrcTimePointT,
typename DstDurationT = typename DstTimePointT::duration,
typename SrcDurationT = typename SrcTimePointT::duration,
typename DstClockT = typename DstTimePointT::clock,
typename SrcClockT = typename SrcTimePointT::clock
>
DstTimePointT
clock_cast_2nd(const SrcTimePointT tp,
const SrcDurationT tolerance = std::chrono::nanoseconds {100},
const int limit = 10)
{
assert(limit > 0);
auto itercnt = 0;
auto src_now = SrcTimePointT {};
auto dst_now = DstTimePointT {};
auto epsilon = detail::max_duration<SrcDurationT>();
do
{
const auto src_before = SrcClockT::now();
const auto dst_between = DstClockT::now();
const auto src_after = SrcClockT::now();
const auto src_diff = src_after - src_before;
const auto delta = detail::abs_duration(src_diff);
if (delta < epsilon)
{
src_now = src_before + src_diff / 2;
dst_now = dst_between;
epsilon = delta;
}
if (++itercnt >= limit)
break;
}
while (epsilon > tolerance);
#ifdef GLOBAL_ITERATION_COUNTER
GLOBAL_ITERATION_COUNTER = itercnt;
#endif
return dst_now + (tp - src_now);
}
Функция принимает два дополнительных необязательных параметра, чтобы указать желаемую точность и максимальное количество итераций, и возвращает текущее наилучшее значение, когда любое из условий становится истинным.
Я использую следующие две простые вспомогательные функции в приведенном выше коде.
namespace detail
{
template <typename DurationT, typename ReprT = typename DurationT::rep>
constexpr DurationT
max_duration() noexcept
{
return DurationT {std::numeric_limits<ReprT>::max()};
}
template <typename DurationT>
constexpr DurationT
abs_duration(const DurationT d) noexcept
{
return DurationT {(d.count() < 0) ? -d.count() : d.count()};
}
}
Распределение ошибок теперь очень симметрично относительно нуля, а величина ошибки уменьшилась почти в 100 раз.
Мне было любопытно, как часто итерация будет выполняться в среднем, поэтому я добавил #ifdef
к коду и #define
добавил его к имени глобальной static
переменной, которую будет распечатывать main
функция. (Обратите внимание, что мы собираем два числа итераций за эксперимент, поэтому эта гистограмма имеет размер выборки 100 000.)
Гистограмма для утверждало случае, с другой стороны, кажется более равномерным. У меня нет объяснения этому, и я ожидал обратного.
Как кажется, мы почти всегда достигаем предела числа итераций (но это нормально), а иногда мы возвращаемся рано. Конечно, на форму этой гистограммы может влиять изменение значений tolerance
и limit
передаваемых функции.
Наконец, я подумал, что могу быть умным и вместо того, чтобы смотреть на src_diff
использовать ошибку src_diff
напрямую в качестве критерия качества.
template
<
typename DstTimePointT,
typename SrcTimePointT,
typename DstDurationT = typename DstTimePointT::duration,
typename SrcDurationT = typename SrcTimePointT::duration,
typename DstClockT = typename DstTimePointT::clock,
typename SrcClockT = typename SrcTimePointT::clock
>
DstTimePointT
clock_cast_3rd(const SrcTimePointT tp,
const SrcDurationT tolerance = std::chrono::nanoseconds {100},
const int limit = 10)
{
assert(limit > 0);
auto itercnt = 0;
auto current = DstTimePointT {};
auto epsilon = detail::max_duration<SrcDurationT>();
do
{
const auto dst = clock_cast_0th<DstTimePointT>(tp);
const auto src = clock_cast_0th<SrcTimePointT>(dst);
const auto delta = detail::abs_duration(src - tp);
if (delta < epsilon)
{
current = dst;
epsilon = delta;
}
if (++itercnt >= limit)
break;
}
while (epsilon > tolerance);
#ifdef GLOBAL_ITERATION_COUNTER
GLOBAL_ITERATION_COUNTER = itercnt;
#endif
return current;
}
Оказывается, это была не очень хорошая идея.
Мы снова вернулись к несимметричному распределению ошибок, и величина ошибки также увеличилась. (Хотя функция также стала дороже!) На самом деле гистограмма для случая простоя выглядит странно. Может ли быть так, что шипы соответствуют тому, как часто мы прерываемся? Это на самом деле не имеет смысла.
Частота итераций показывает ту же тенденцию, что и раньше.
В заключение я бы порекомендовал использовать 2- й подход, и я думаю, что значения по умолчанию для необязательных параметров являются разумными, но, конечно, это может варьироваться от машины к машине. Говард Хиннант отметил, что ограничение в четыре итерации ему подходит.
Если вы реализуете это по-настоящему, вы не хотели бы упустить возможность оптимизации, чтобы проверить, является ли std::is_same<SrcClockT, DstClockT>::value
и в этом случае просто применить std::chrono::time_point_cast
никогда не вызывая ничего now
функция (и, следовательно, не вносит ошибки).
Если вы хотите повторить мои эксперименты, я предоставлю полный код здесь. clock_cast XYZ
код clock_cast XYZ
уже завершен. (Просто объедините все примеры в один файл, #include
очевидные заголовки и сохраните как clock_cast.hxx
.)
Вот фактический main.cxx
который я использовал.
#include <iomanip>
#include <iostream>
#ifdef GLOBAL_ITERATION_COUNTER
static int GLOBAL_ITERATION_COUNTER;
#endif
#include "clock_cast.hxx"
int
main()
{
using namespace std::chrono;
const auto now = system_clock::now();
const auto steady_now = CLOCK_CAST<steady_clock::time_point>(now);
#ifdef GLOBAL_ITERATION_COUNTER
std::cerr << std::setw(8) << GLOBAL_ITERATION_COUNTER << '\n';
#endif
const auto system_now = CLOCK_CAST<system_clock::time_point>(steady_now);
#ifdef GLOBAL_ITERATION_COUNTER
std::cerr << std::setw(8) << GLOBAL_ITERATION_COUNTER << '\n';
#endif
const auto diff = system_now - now;
std::cout << std::setw(8) << duration_cast<nanoseconds>(diff).count() << '\n';
}
Следующий GNUmakefile
и запускает все.
CXX = g++ -std=c++14
CPPFLAGS = -DGLOBAL_ITERATION_COUNTER=global_counter
CXXFLAGS = -Wall -Wextra -Werror -pedantic -O2 -g
runs = 50000
cutoff = 0.999
execfiles = zeroth.exe first.exe second.exe third.exe
datafiles = \
zeroth.dat \
first.dat \
second.dat second_iterations.dat \
third.dat third_iterations.dat
picturefiles = ${datafiles:.dat=.png}
all: ${picturefiles}
zeroth.png: errors.gp zeroth.freq
TAG='zeroth' TITLE="0th Approach ${SUBTITLE}" MICROS=0 gnuplot $<
first.png: errors.gp first.freq
TAG='first' TITLE="1st Approach ${SUBTITLE}" MICROS=0 gnuplot $<
second.png: errors.gp second.freq
TAG='second' TITLE="2nd Approach ${SUBTITLE}" gnuplot $<
second_iterations.png: iterations.gp second_iterations.freq
TAG='second' TITLE="2nd Approach ${SUBTITLE}" gnuplot $<
third.png: errors.gp third.freq
TAG='third' TITLE="3rd Approach ${SUBTITLE}" gnuplot $<
third_iterations.png: iterations.gp third_iterations.freq
TAG='third' TITLE="3rd Approach ${SUBTITLE}" gnuplot $<
zeroth.exe: main.cxx clock_cast.hxx
${CXX} -o [email protected] ${CPPFLAGS} -DCLOCK_CAST='clock_cast_0th' ${CXXFLAGS} $<
first.exe: main.cxx clock_cast.hxx
${CXX} -o [email protected] ${CPPFLAGS} -DCLOCK_CAST='clock_cast_1st' ${CXXFLAGS} $<
second.exe: main.cxx clock_cast.hxx
${CXX} -o [email protected] ${CPPFLAGS} -DCLOCK_CAST='clock_cast_2nd' ${CXXFLAGS} $<
third.exe: main.cxx clock_cast.hxx
${CXX} -o [email protected] ${CPPFLAGS} -DCLOCK_CAST='clock_cast_3rd' ${CXXFLAGS} $<
%.freq: binput.py %.dat
python $^ ${cutoff} > [email protected]
${datafiles}: ${execfiles}
${SHELL} -eu run.sh ${runs} $^
clean:
rm -f *.exe *.dat *.freq *.png
.PHONY: all clean
Вспомогательный скрипт run.sh
довольно прост. В качестве улучшения предыдущей версии этого ответа я теперь выполняю различные программы во внутреннем цикле, чтобы быть более справедливыми и, возможно, также лучше избавиться от эффектов кэширования.
#! /bin/bash -eu
n="$1"
shift
for exe in "[email protected]"
do
name="${exe%.exe}"
rm -f "${name}.dat" "${name}_iterations.dat"
done
i=0
while [ $i -lt $n ]
do
for exe in "[email protected]"
do
name="${exe%.exe}"
"./${exe}" 1>>"${name}.dat" 2>>"${name}_iterations.dat"
done
i=$(($i + 1))
done
И я также написал сценарий binput.py
потому что я не мог понять, как делать гистограммы только в Gnuplot.
#! /usr/bin/python3
import sys
import math
def main():
cutoff = float(sys.argv[2]) if len(sys.argv) >= 3 else 1.0
with open(sys.argv[1], 'r') as istr:
values = sorted(list(map(float, istr)), key=abs)
if cutoff < 1.0:
values = values[:int((cutoff - 1.0) * len(values))]
min_val = min(values)
max_val = max(values)
binsize = 1.0
if max_val - min_val > 50:
binsize = (max_val - min_val) / 50
bins = int(1 + math.ceil((max_val - min_val) / binsize))
histo = [0 for i in range(bins)]
print("minimum: {:16.6f}".format(min_val), file=sys.stderr)
print("maximum: {:16.6f}".format(max_val), file=sys.stderr)
print("binsize: {:16.6f}".format(binsize), file=sys.stderr)
for x in values:
idx = int((x - min_val) / binsize)
histo[idx] += 1
for (i, n) in enumerate(histo):
value = min_val + i * binsize
frequency = n / len(values)
print('{:16.6e} {:16.6e}'.format(value, frequency))
if __name__ == '__main__':
main()
Наконец, вот errors.gp
tag = system('echo ${TAG-hist}')
file_hist = sprintf('%s.freq', tag)
file_plot = sprintf('%s.png', tag)
micros_eh = 0 + system('echo ${MICROS-0}')
set terminal png size 600,450
set output file_plot
set title system('echo ${TITLE-Errors}')
if (micros_eh) { set xlabel "error / µs" } else { set xlabel "error / ns" }
set ylabel "relative frequency"
set xrange [* : *]
set yrange [1.0e-5 : 1]
set log y
set format y '10^{%T}'
set format x '%g'
set style fill solid 0.6
factor = micros_eh ? 1.0e-3 : 1.0
plot file_hist using (factor * $1):2 with boxes notitle lc '#cc0000'
... и сценарии iterations.gp
.
tag = system('echo ${TAG-hist}')
file_hist = sprintf('%s_iterations.freq', tag)
file_plot = sprintf('%s_iterations.png', tag)
set terminal png size 600,450
set output file_plot
set title system('echo ${TITLE-Iterations}')
set xlabel "iterations"
set ylabel "frequency"
set xrange [0 : *]
set yrange [1.0e-5 : 1]
set xtics 1
set xtics add ('' 0)
set log y
set format y '10^{%T}'
set format x '%g'
set boxwidth 1.0
set style fill solid 0.6
plot file_hist using 1:2 with boxes notitle lc '#3465a4'