Suma estable eficiente de números ordenados

12

Tengo una lista bastante larga de números positivos de coma flotante ( std::vector<float>, tamaño ~ 1000). Los números se ordenan en orden decreciente. Si los sumo siguiendo el orden:

for (auto v : vec) { sum += v; }

Supongo que puedo tener algún problema de estabilidad numérica, ya que cerca del final del vector sumserá mucho mayor que v. La solución más fácil sería atravesar el vector en orden inverso. Mi pregunta es: ¿es tan eficiente como el caso avanzado? ¿Me faltará más caché?

¿Hay alguna otra solución inteligente?

Ruggero Turra
fuente
1
La pregunta de velocidad es fácil de responder. Benchmark it.
Davide Spataro
¿Es la velocidad más importante que la precisión?
rígido
No es un duplicado, pero es una pregunta muy similar: suma de series usando flotante
acraig5075
44
Puede que tenga que prestar atención a los números negativos.
Programador
3
Si realmente te importa la precisión en grados altos, mira el resumen de Kahan .
Max Langhof

Respuestas:

3

Supongo que puedo tener algún problema de estabilidad numérica

Así que pruébalo. Actualmente tiene un problema hipotético, es decir, no hay ningún problema.

Si realiza la prueba y la hipótesis se materializa en un problema real , entonces debería preocuparse por solucionarlo.

Es decir, la precisión de punto flotante puede causar problemas, pero puede confirmar si realmente lo hace para sus datos, antes de priorizar eso sobre todo lo demás.

... me faltará más caché?

Mil flotadores son 4Kb: cabe en la memoria caché de un sistema moderno de mercado masivo (si tiene otra plataforma en mente, díganos de qué se trata).

El único riesgo es que el prefetcher no lo ayudará al iterar hacia atrás, pero, por supuesto, su vector ya puede estar en caché. Realmente no puede determinar esto hasta que realice un perfil en el contexto de su programa completo, por lo que no tiene sentido preocuparse hasta que tenga un programa completo.

¿Hay alguna otra solución inteligente?

No se preocupe por las cosas que podrían convertirse en problemas, hasta que realmente se conviertan en problemas. A lo más, vale la pena señalar posibles problemas y estructurar su código para que pueda reemplazar la solución más simple posible con una cuidadosamente optimizada más adelante, sin volver a escribir todo lo demás.

Inútil
fuente
5

Marqué en el banco su caso de uso y los resultados (ver imagen adjunta) apuntan a la dirección en la que no hay ninguna diferencia de rendimiento para avanzar o retroceder.

Es posible que también desee medir en su compilador de hardware +.


Usar STL para realizar la suma es tan rápido como el bucle manual sobre datos, pero mucho más expresivo.

use lo siguiente para la acumulación inversa:

std::accumulate(rbegin(data), rend(data), 0.0f);

mientras que para la acumulación hacia adelante:

std::accumulate(begin(data), end(data), 0.0f);

ingrese la descripción de la imagen aquí

Davide Spataro
fuente
ese sitio web es súper genial. Solo para estar seguro: no estás cronometrando la generación aleatoria, ¿verdad?
Ruggero Turra
No, solo la parte del statebucle está cronometrada.
Davide Spataro
2

La solución más fácil sería atravesar el vector en orden inverso. Mi pregunta es: ¿es tan eficiente como el caso avanzado? ¿Me faltará más caché?

Sí, es eficiente. La predicción de sucursales y la estrategia de caché inteligente de su hardware están ajustadas para acceso secuencial. Puede acumular su vector de manera segura:

#include <numeric>

auto const sum = std::accumulate(crbegin(v), crend(v), 0.f);
YSC
fuente
2
¿Puede aclarar: en este contexto "acceso secuencial" significa hacia adelante, hacia atrás o ambos?
Ruggero Turra
1
@RuggeroTurra No puedo a menos que pueda encontrar una fuente, y no estoy de humor para leer las hojas de datos de la CPU en este momento.
YSC
@RuggeroTurra Por lo general, el acceso secuencial significaría hacia adelante. Todos los prefetchers de memoria semi-decentes atrapan el acceso secuencial.
Cepillo de dientes
@ Cepillo de dientes, gracias. Entonces, si hago un bucle hacia atrás, en principio, puede ser un problema de rendimiento
Ruggero Turra
En principio, en al menos algún hardware, si el vector completo no está ya en la caché L1.
Inútil el
2

Para este propósito, puede usar el iterador inverso sin ninguna transposición en su std::vector<float> vec:

float sum{0.f};
for (auto rIt = vec.rbegin(); rIt!= vec.rend(); ++rIt)
{
    sum += *rit;
}

O haga el mismo trabajo usando algortithm estándar:

float sum = std::accumulate(vec.crbegin(), vec.crend(), 0.f);

El rendimiento debe ser el mismo, solo cambió la dirección de derivación de su vector

Malov Vladimir
fuente
Corrígeme si me equivoco, pero creo que esto es aún más eficiente que la declaración foreach que usa OP, ya que introduce una sobrecarga. YSC tiene razón sobre la parte de estabilidad numérica, aunque.
sephiroth
44
@sephiroth No, a cualquier compilador medio decente realmente no le importará si escribiste un rango o un iterador.
Max Langhof
1
No se garantiza que el rendimiento en el mundo real sea el mismo, debido a los cachés / captación previa. Es razonable que el OP tenga cuidado con eso.
Max Langhof
1

Si por estabilidad numérica quiere decir precisión, entonces sí, puede terminar con problemas de precisión. Dependiendo de la proporción de los valores más grandes a los más pequeños, y sus requisitos de precisión en el resultado, esto puede o no ser un problema.

Si desea tener una alta precisión, considere la suma de Kahan : esto utiliza un flotador adicional para la compensación de errores. También hay suma por pares .

Para un análisis detallado de la compensación entre precisión y tiempo, consulte este artículo .

ACTUALIZACIÓN para C ++ 17:

Algunas de las otras respuestas mencionan std::accumulate. Desde C ++ 17 existen políticas de ejecución que permiten paralelizar algoritmos.

Por ejemplo

#include <vector>
#include <execution>
#include <iostream>
#include <numeric>

int main()
{  
   std::vector<double> input{0.1, 0.9, 0.2, 0.8, 0.3, 0.7, 0.4, 0.6, 0.5};

   double reduceResult = std::reduce(std::execution::par, std::begin(input), std::end(input));

   std:: cout << "reduceResult " << reduceResult << '\n';
}

Esto debería hacer que la suma de conjuntos de datos grandes sea más rápida a costa de errores de redondeo no deterministas (supongo que el usuario no podrá determinar la partición de subprocesos).

Paul Floyd
fuente