c++ c++11 chrono

Convertir entre c++ 11 relojes



c++11 chrono (2)

Si tengo un time_point de time_point para un reloj arbitrario (por ejemplo, high_resolution_clock::time_point ), ¿hay una manera de convertirlo en un time_point de time_point para otro reloj arbitrario (por ejemplo, system_clock::time_point )?

Sé que tendría que haber límites, si existiera esta habilidad, porque no todos los relojes son estables, pero ¿existe alguna funcionalidad para ayudar a tales conversiones en la especificación?


Me preguntaba si podría mejorarse la precisión de la conversión propuesta por T.C. y Howard Hinnant . Para referencia, aquí está la versión base que probé.

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); }

Usando la prueba

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''; }

donde CLOCK_CAST sería #define d to, por ahora, clock_cast_0th , recolecté un histograma para un sistema inactivo y uno bajo carga alta. Tenga en cuenta que esta es una prueba de arranque en frío. Primero intenté llamar a la función en un bucle donde da mejores resultados. Sin embargo, creo que esto daría una impresión falsa porque la mayoría de los programas del mundo real probablemente conviertan un punto en el tiempo de vez en cuando y afectarán el caso frío.

La carga se generó ejecutando las siguientes tareas paralelas al programa de prueba. (Mi computadora tiene cuatro CPUs.)

  • Un punto de referencia de multiplicación de matrices (de un solo hilo).
  • 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

Esos comandos que terminarían en tiempo finito se ejecutaron en un bucle infinito.

El siguiente histograma, así como los que seguirán, muestra los errores de 50 000 ejecuciones con el peor 1 eliminado.

Tenga en cuenta que la ordenada tiene una escala logarítmica.

Los errores caen aproximadamente en el rango entre 0.5 µs y 1.0 µs en el caso inactivo y 0.5 µs y 1.5 µs en el caso en disputa.

La observación más sorprendente es que la distribución de errores está lejos de ser simétrica (no hay errores negativos en absoluto), lo que indica un gran componente sistemático en el error. Esto tiene sentido porque si nos interrumpen entre las dos llamadas now , el error es siempre en la misma dirección y no podemos ser interrumpidos por un "tiempo negativo".

El histograma para el caso en disputa casi parece una distribución exponencial perfecta (¡tenga en cuenta la escala de registro!) Con un corte bastante agudo que parece plausible; la posibilidad de que te interrumpan por el tiempo t es aproximadamente proporcional a e - t .

Entonces intenté usar el siguiente truco

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); }

esperando que la interpolación scr_now cancele parcialmente el error introducido al llamar inevitablemente a los relojes en orden secuencial.

En la primera versión de esta respuesta, afirmé que esto no ayudaba en nada. Resulta que esto no era cierto. Después de que Howard Hinnant señaló que sí observó mejoras, mejoré mis pruebas y ahora hay algunas mejoras observables.

No fue tanto una mejora en términos de la extensión del error, sin embargo, los errores ahora se centran aproximadamente en cero, lo que significa que ahora tenemos errores en el rango de −0.5 & # 1202f; µs a 0.5 & # 1202f; µs . La distribución más simétrica indica que el componente estadístico del error se hizo más dominante.

A continuación, intenté llamar el código anterior en un bucle que escogería el mejor valor para 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); }

La función toma dos parámetros opcionales adicionales para especificar la precisión deseada y el número máximo de iteraciones y devuelve el mejor valor actual cuando cualquiera de las condiciones se vuelve verdadera.

Estoy usando las siguientes dos funciones auxiliares directas en el código anterior.

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()}; } }

La distribución de errores ahora es muy simétrica en torno a cero y la magnitud del error se redujo hasta en un factor de casi 100.

Tenía curiosidad por la frecuencia con la que la iteración se ejecutaría en promedio, así que agregué #ifdef al código y #define d al nombre de una variable static global que la función main imprimirá. (Tenga en cuenta que recopilamos dos recuentos de iteraciones por experimento, por lo que este histograma tiene un tamaño de muestra de 100 000).

El histograma para el caso contendido, por otro lado, parece más uniforme. No tengo una explicación para esto y habría esperado lo contrario.

Como parece, casi siempre alcanzamos el límite de recuento de iteraciones (pero está bien) y, a veces, regresamos antes. La forma de este histograma, por supuesto, puede verse influida por la alteración de los valores de tolerance y limit pasados ​​a la función.

Finalmente, pensé que podría ser inteligente y en lugar de ver a src_diff usar el error de ida y vuelta directamente como criterio de calidad.

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; }

Resulta que esta no era una buena idea.

Hemos vuelto a una distribución de error no simétrica y la magnitud del error también ha aumentado. (¡Mientras que la función también se volvió más costosa!) En realidad, el histograma para el caso inactivo parece extraño . ¿Podría ser que los picos se correspondan con la frecuencia con que nos interrumpen? Esto en realidad no tiene sentido.

La frecuencia de iteración muestra la misma tendencia que antes.

En conclusión, recomendaría utilizar el segundo enfoque y creo que los valores predeterminados para los parámetros opcionales son razonables pero, por supuesto, esto es algo que puede variar de una máquina a otra. Howard Hinnant ha comentado que un límite de solo cuatro iteraciones funcionó bien para él.

Si implementas esto de verdad, no te gustaría perder la oportunidad de optimización para verificar si std::is_same<SrcClockT, DstClockT>::value y, en este caso, simplemente aplica std::chrono::time_point_cast sin llamar nunca a now funciona (y por lo tanto no introduce error).

En caso de que quiera repetir mis experimentos, le proporciono el código completo aquí. El código clock_cast XYZ ya está completo. (Solo concatene todos los ejemplos en un solo archivo, #include los encabezados obvios y guárdelos como clock_cast.hxx .)

Aquí está el main.cxx real que utilicé.

#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''; }

El siguiente GNUmakefile construye y ejecuta todo.

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 $@ ${CPPFLAGS} -DCLOCK_CAST=''clock_cast_0th'' ${CXXFLAGS} $< first.exe: main.cxx clock_cast.hxx ${CXX} -o $@ ${CPPFLAGS} -DCLOCK_CAST=''clock_cast_1st'' ${CXXFLAGS} $< second.exe: main.cxx clock_cast.hxx ${CXX} -o $@ ${CPPFLAGS} -DCLOCK_CAST=''clock_cast_2nd'' ${CXXFLAGS} $< third.exe: main.cxx clock_cast.hxx ${CXX} -o $@ ${CPPFLAGS} -DCLOCK_CAST=''clock_cast_3rd'' ${CXXFLAGS} $< %.freq: binput.py %.dat python $^ ${cutoff} > $@ ${datafiles}: ${execfiles} ${SHELL} -eu run.sh ${runs} $^ clean: rm -f *.exe *.dat *.freq *.png .PHONY: all clean

El script auxiliar run.sh es bastante simple. Como mejora a una versión anterior de esta respuesta, ahora estoy ejecutando los diferentes programas en el bucle interno para ser más justos y tal vez también es mejor deshacerme de los efectos de almacenamiento en caché.

#! /bin/bash -eu n="$1" shift for exe in "$@" do name="${exe%.exe}" rm -f "${name}.dat" "${name}_iterations.dat" done i=0 while [ $i -lt $n ] do for exe in "$@" do name="${exe%.exe}" "./${exe}" 1>>"${name}.dat" 2>>"${name}_iterations.dat" done i=$(($i + 1)) done

Y también escribí el script binput.py porque no pude averiguar cómo hacer los histogramas solo con 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()

Finalmente, aquí están los 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''

... y iterations.gp scripts.

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''


No hay forma de hacer esto precisamente a menos que sepa la diferencia de duración precisa entre las dos épocas del reloj. Y no sabes esto para high_resolution_clock y is_same<high_resolution_clock, system_clock>{} menos que is_same<high_resolution_clock, system_clock>{} es true .

Dicho esto, puede programar una traducción aproximadamente correcta y se parece mucho a lo que dice en su comentario. De hecho, libc ++ juega este truco en su implementación de condition_variable::wait_for :

https://github.com/llvm-mirror/libcxx/blob/master/include/__mutex_base#L385-L386

Las llamadas a now de los diferentes relojes se hacen lo más cerca posible, y se espera que el hilo no se anule entre estas dos llamadas durante demasiado tiempo. Es lo mejor que sé hacer, y la especificación tiene margen de maniobra para permitir este tipo de chanchullos. Por ejemplo, se permite que algo se despierte un poco tarde, pero no un poco temprano.

En el caso de libc ++, el sistema operativo subyacente solo sabe cómo esperar en system_clock::time_point , pero la especificación dice que debe esperar en steady_clock (por buenas razones). Así que haces lo que puedes.

Aquí hay un bosquejo de HelloWorld de la idea:

#include <chrono> #include <iostream> std::chrono::system_clock::time_point to_system(std::chrono::steady_clock::time_point tp) { using namespace std::chrono; auto sys_now = system_clock::now(); auto sdy_now = steady_clock::now(); return time_point_cast<system_clock::duration>(tp - sdy_now + sys_now); } std::chrono::steady_clock::time_point to_steady(std::chrono::system_clock::time_point tp) { using namespace std::chrono; auto sdy_now = steady_clock::now(); auto sys_now = system_clock::now(); return tp - sys_now + sdy_now; } int main() { using namespace std::chrono; auto now = system_clock::now(); std::cout << now.time_since_epoch().count() << ''/n''; auto converted_now = to_system(to_steady(now)); std::cout << converted_now.time_since_epoch().count() << ''/n''; }

Para mí, usando Apple clang / libc ++ en -O3 esta salida:

1454985476610067 1454985476610073

indicando que la conversión combinada tuvo un error de 6 microsegundos.

Actualizar

He invertido arbitrariamente el orden de las llamadas a now() en una de las conversiones anteriores, de modo que una conversión las llama en una orden, y la otra las llama en el orden inverso. Esto no debería tener ningún impacto en la precisión de cualquier conversión. Sin embargo, al convertir ambas formas como lo hago en este HelloWorld, debería haber una cancelación estadística que ayude a reducir el error de conversión de ida y vuelta .