Diferencia en el rendimiento: std :: acumular vs std :: inner_product vs Loop

Hoy, quiero compartir algo que me dejó boquiabierto cuando intenté implementar esta operación simple:

Encontré diferentes formas de realizar la misma operación:

Usando lastd::inner_product.Implementar un predicado y usar lastd::accumulate función.Utilizando un bucle en estilo C.

Quería realizar algunos puntos de referencia utilizando Quick Bench y habilitando todas las optimizaciones.

En primer lugar, comparé las dos alternativas de C ++ con valores flotantes. Este es el código utilizado al usarstd::accumulate:

const auto predicate = [](const double previous, const double current) {
    return previous + current * current;
};
const auto result = std::accumulate(input.cbegin(), input.cend(), 0, predicate);

Versus este código usando elstd::inner_product funcionalidad:

const auto result = std::inner_product(input.cbegin(), input.cend(), input.cbegin(), 1);

Después de ejecutar el punto de referencia con toda la optimización habilitada, obtuve este resultado:

Ambos algoritmos parecen alcanzar el mismo rendimiento. Quería ir más allá y probar la implementación de C:

double result = 0;
for (auto i = 0; i < input.size(); ++i) {
  result += input[i] * input[i];
}

Y sorprendentemente, encontré:

No esperaba este resultado. Estaba seguro de que hay algo mal, así que verifiqué la implementación de GCC:

template<typename _InputIterator1, typename _InputIterator2, typename _Tp>
inline _Tp
inner_product(_InputIterator1 __first1, _InputIterator1 __last1,
      _InputIterator2 __first2, _Tp __init)
{
  // concept requirements
  __glibcxx_function_requires(_InputIteratorConcept<_InputIterator1>)
  __glibcxx_function_requires(_InputIteratorConcept<_InputIterator2>)
  __glibcxx_requires_valid_range(__first1, __last1);

  for (; __first1 != __last1; ++__first1, (void)++__first2)
__init = __init + (*__first1 * *__first2);
  return __init;
}

Encontré que estaba haciendo lo mismo que la implementación de C. Después de revisar la implementación, descubrí algo extraño (o al menos no esperaba tener ese impacto significativo): en todas las acumulaciones internas, estaba haciendo una conversión del iterador value_type al tipo del valor inicial.

En mi caso, estaba inicializando los valores iniciales a 0 o 1, los valores se consideraban enteros y en cada acumulación, el compilador estaba haciendo el casting. En los diferentes casos de prueba, mi matriz de entrada almacena puntos flotantes truncados, por lo que el resultado no cambió.

Después de actualizar el valor inicial a un tipo doble:

const auto result = std::accumulate(input.cbegin(), input.cend(), 0.0, predicate);

Y

const auto result = std::inner_product(input.cbegin(), input.cend(), input.cbegin(), 0.0);

Obtuve el resultado esperado:

Ahora, entiendo que dejar que el valor inicial sea un tipo independiente del tipo subyacente del iterador puede hacer que la función sea más flexible y permitir hacer más cosas. Pero

Si estoy acumulando elementos de una matriz, espero obtener el mismo tipo como resultado. Lo mismo para el producto interno.

Debería ser el comportamiento predeterminado?

¿Por qué el estándar decidió realizarlo de esta manera?

Respuestas a la pregunta(1)

Su respuesta a la pregunta