c++ - ¿Por qué es std:: async lento en comparación con los simples hilos separados?
multithreading c++11 (2)
Una diferencia clave es que el futuro devuelto por async se une al hilo cuando el futuro se destruye, o en su caso, se reemplaza con un nuevo valor.
Esto significa que tiene que ejecutar someTask()
y unirse al hilo, los cuales llevan tiempo. Ninguna de tus otras pruebas lo está haciendo, donde simplemente las generan de forma independiente.
Me han dicho varias veces que debo usar el tipo de tareas std::async
para el fuego y el olvido con el parámetro std::launch::async
(de modo que hace su magia en un nuevo hilo de ejecución, preferiblemente).
Animado por estas declaraciones, quería ver cómo se compara std::async
con:
- ejecución secuencial
- un simple desacoplado
std::thread
- mi simple async "implementacion"
Mi implementación asíncrona ingenua se ve así:
template <typename F, typename... Args>
auto myAsync(F&& f, Args&&... args) -> std::future<decltype(f(args...))>
{
std::packaged_task<decltype(f(args...))()> task(std::bind(std::forward<F>(f), std::forward<Args>(args)...));
auto future = task.get_future();
std::thread thread(std::move(task));
thread.detach();
return future;
}
Nada especial aquí, empaqueta el functor f
en una std::packaged task
junto con sus argumentos, lo lanza en un nuevo std::thread
que se separa, y regresa con std::future
de la tarea.
Y ahora el código que mide el tiempo de ejecución con std::chrono::high_resolution_clock
:
int main(void)
{
constexpr unsigned short TIMES = 1000;
auto start = std::chrono::high_resolution_clock::now();
for (int i = 0; i < TIMES; ++i)
{
someTask();
}
auto dur = std::chrono::high_resolution_clock::now() - start;
auto tstart = std::chrono::high_resolution_clock::now();
for (int i = 0; i < TIMES; ++i)
{
std::thread t(someTask);
t.detach();
}
auto tdur = std::chrono::high_resolution_clock::now() - tstart;
std::future<void> f;
auto astart = std::chrono::high_resolution_clock::now();
for (int i = 0; i < TIMES; ++i)
{
f = std::async(std::launch::async, someTask);
}
auto adur = std::chrono::high_resolution_clock::now() - astart;
auto mastart = std::chrono::high_resolution_clock::now();
for (int i = 0; i < TIMES; ++i)
{
f = myAsync(someTask);
}
auto madur = std::chrono::high_resolution_clock::now() - mastart;
std::cout << "Simple: " << std::chrono::duration_cast<std::chrono::microseconds>(dur).count() <<
std::endl << "Threaded: " << std::chrono::duration_cast<std::chrono::microseconds>(tdur).count() <<
std::endl << "std::sync: " << std::chrono::duration_cast<std::chrono::microseconds>(adur).count() <<
std::endl << "My async: " << std::chrono::duration_cast<std::chrono::microseconds>(madur).count() << std::endl;
return EXIT_SUCCESS;
}
Donde someTask()
es un método simple, donde espero un poco, simulando un trabajo realizado:
void someTask()
{
std::this_thread::sleep_for(std::chrono::milliseconds(1));
}
Finalmente, mis resultados:
- Secuencial: 1263615
- Roscado: 47111
- std :: sync: 821441
- Mi asíncrono: 30784
¿Alguien podría explicar estos resultados ? Parece que std::aysnc
es mucho más lento que mi implementación ingenua , o simplemente un std::thread
s separado simple y simple. ¿Por qué es eso? Después de estos resultados, ¿hay alguna razón para usar std::async
?
(Tenga en cuenta que también hice este punto de referencia con clang ++ y g ++, y los resultados fueron muy similares)
ACTUALIZAR:
Después de leer la respuesta de Dave S, actualicé mi pequeño punto de referencia de la siguiente manera:
std::future<void> f[TIMES];
auto astart = std::chrono::high_resolution_clock::now();
for (int i = 0; i < TIMES; ++i)
{
f[i] = std::async(std::launch::async, someTask);
}
auto adur = std::chrono::high_resolution_clock::now() - astart;
Así que los std::future
s ahora no se destruyen, y por lo tanto se unen, en cada ejecución. Después de este cambio en el código, std::async
produce resultados similares a mi implementación y desvinculado std::thread
s.
sts::async
devuelve un std::future
especial std::future
. Este futuro tiene un ~future
que hace un .wait()
.
Así que sus ejemplos son fundamentalmente diferentes. Los lentos realmente hacen las tareas durante su tiempo. Los más rápidos simplemente ponen en cola las tareas y se olvidan de cómo saber cuándo se realiza la tarea. Como el comportamiento de los programas que permiten que los hilos duren más allá del final de main es impredecible, uno debe evitarlo.
La forma correcta de comparar las tareas es almacenar el future
resultante cuando genersting, y antes de que finalice el temporizador, ya sea .wait()
/ .join()
todos, o evite destruir los objetos hasta que el temporizador expire. Este último caso, sin embargo, hace que la versión sewuential se vea peor de lo que es.
Debe unirse / esperar antes de comenzar la próxima prueba, ya que de lo contrario está robando recursos de su tiempo.
Tenga en cuenta que los futuros movidos eliminan la espera de la fuente.