c# - statement - Math.Max vs en línea si-¿cuáles son las diferencias?
inline if statement c# example (7)
si la declaración se considera beneficiosa
Resumen
una declaración de la forma if (a > max) max = a
es la forma más rápida de determinar el máximo de un conjunto de números. Sin embargo, la infraestructura del bucle en sí toma la mayor parte del tiempo de CPU, por lo que esta optimización es cuestionable al final.
Detalles
La respuesta de luisperezphd es interesante porque proporciona números, sin embargo, creo que el método es defectuoso: es muy probable que el compilador mueva la comparación fuera del bucle, por lo que la respuesta no mide lo que quiere medir. Esto explica la diferencia de tiempo insignificante entre el bucle de control y los bucles de medición.
Para evitar esta optimización del bucle, agregué una operación que depende de la variable del bucle, al bucle de control vacío, así como a todos los bucles de medición. Simulo el caso de uso común de encontrar el máximo en una lista de números y usé tres conjuntos de datos:
- mejor caso: el primer número es el máximo, todos los números después de él son más pequeños
- El peor de los casos: cada número es más grande que el anterior, por lo que el máximo cambia cada iteración
- caso promedio: un conjunto de números aleatorios
Vea a continuación el código.
El resultado fue bastante sorprendente para mí. En mi computadora portátil Core i5 2520M obtuve lo siguiente para mil millones de iteraciones (el control vacío tomó alrededor de 2.6 segundos en todos los casos):
-
max = Math.Max(max, a)
: 2.0 seg. mejor caso / 1.3 seg. peor caso / 2.0 seg. caso promedio -
max = Math.Max(a, max)
: 1,6 seg. mejor caso / 2,0 seg. peor caso / 1,5 seg. caso promedio -
max = max > a ? max : a
max = max > a ? max : a
: 1.2 seg. mejor caso / 1.2 seg. peor caso / 1.2 seg. caso promedio -
if (a > max) max = a
: 0.2 seg. mejor caso / 0.9 seg. peor caso / 0.3 seg. caso promedio
Así que, a pesar de las largas tuberías de la CPU y las penalizaciones resultantes para la bifurcación, la afirmación antigua es el claro ganador de todos los conjuntos de datos simulados; en el mejor de los casos, es 10 veces más rápido que Math.Max
, y en el peor de los casos, aún más rápido en un 30%.
Otra sorpresa es que el orden de los argumentos a Math.Max
importa. Presumiblemente, esto se debe a que la lógica de predicción de la rama de la CPU funciona de manera diferente para los dos casos, y predecir erróneamente las ramas según el orden de los argumentos.
Sin embargo, la mayoría del tiempo de CPU se gasta en la infraestructura de bucle, por lo que al final esta optimización es, en el mejor de los casos, cuestionable. Proporciona una reducción medible pero menor en el tiempo total de ejecución.
ACTUALIZADO por luisperezphd
No podía encajar esto como un comentario y tenía más sentido escribirlo aquí en lugar de como parte de mi respuesta para que estuviera en contexto.
Tu teoría tiene sentido, pero no pude reproducir los resultados. Primero, por alguna razón, usando su código, mi bucle de control llevaba más tiempo que los bucles que contienen trabajo.
Por esa razón hice los números aquí en relación con el tiempo más bajo en lugar del ciclo de control. Los segundos en los resultados son cuánto más tomó el tiempo más rápido. Por ejemplo, en los resultados inmediatamente por debajo, el tiempo más rápido fue para el mejor caso de Math.Max (a, max), por lo que cada otro resultado representa cuánto más tardaron.
A continuación se presentan los resultados que obtuve:
-
max = Math.Max(max, a)
: 0.012 seg. mejor caso / 0.007 seg. peor caso / 0.028 seg. caso promedio -
max = Math.Max(a, max)
: 0,000 caso mejor / 0,021 caso peor / caso promedio de 0.019 segundos -
max = max > a ? max : a
max = max > a ? max : a
: 0,022 seg. mejor caso / 0,02 seg. peor caso / 0,01 seg. caso promedio -
if (a > max) max = a
: 0.015 seg. mejor caso / 0.024 seg. peor caso / 0.019 seg. caso promedio
La segunda vez que lo corrí lo conseguí:
-
max = Math.Max(max, a
): 0.024 seg. mejor caso / 0.010 seg. peor caso / 0.009 seg. caso promedio -
max = Math.Max(a, max)
: 0.001 seg. mejor caso / 0.000 seg. peor caso / 0.018 seg. caso promedio -
max = max > a ? max : a
max = max > a ? max : a
: 0,011 seg. mejor caso / 0,005 seg. peor caso / 0,018 seg. caso promedio -
if (a > max) max = a
: 0,000 seg. mejor caso / 0,005 seg. peor caso / 0,039 seg. caso promedio
Hay suficiente volumen en estas pruebas que cualquier anomalía debería haberse eliminado. Sin embargo, a pesar de eso los resultados son bastante diferentes. Tal vez la gran asignación de memoria para la matriz tenga algo que ver con eso. O posiblemente la diferencia sea tan pequeña que cualquier otra cosa que ocurra en la computadora en ese momento es la verdadera causa de la variación.
Tenga en cuenta que el tiempo más rápido, representado en los resultados anteriores por 0.000, es de aproximadamente 8 segundos. Por lo tanto, si considera que el plazo más largo fue 8.039, la variación en el tiempo es aproximadamente la mitad del porcentaje (0.5%), también demasiado pequeña para importar.
El ordenador
El código se ejecutó en Windows 8.1, i7 4810MQ 2.8Ghz y se compiló en .NET 4.0.
Modificaciones de código
Modifiqué un poco su código para generar los resultados en el formato que se muestra arriba. También agregué código adicional para esperar 1 segundo después de comenzar a tener en cuenta el tiempo de carga adicional que pueda necesitar .NET al ejecutar el ensamblaje.
También ejecuté todas las pruebas dos veces para tener en cuenta las optimizaciones de la CPU. Finalmente, cambié el int
por i
a una unit
para poder ejecutar el bucle 4 mil millones de veces en lugar de 1 mil millones para obtener un tiempo más largo.
Probablemente eso sea excesivo, pero es todo para asegurarse de que las pruebas no se vean afectadas por ninguno de esos factores.
Puede encontrar el código en: http://pastebin.com/84qi2cbD
Código
using System;
using System.Diagnostics;
namespace ProfileMathMax
{
class Program
{
static double controlTotalSeconds;
const int InnerLoopCount = 100000;
const int OuterLoopCount = 1000000000 / InnerLoopCount;
static int[] values = new int[InnerLoopCount];
static int total = 0;
static void ProfileBase()
{
Stopwatch stopwatch = new Stopwatch();
stopwatch.Start();
int maxValue;
for (int j = 0; j < OuterLoopCount; j++)
{
maxValue = 0;
for (int i = 0; i < InnerLoopCount; i++)
{
// baseline
total += values[i];
}
}
stopwatch.Stop();
controlTotalSeconds = stopwatch.Elapsed.TotalSeconds;
Console.WriteLine("Control - Empty Loop - " + controlTotalSeconds + " seconds");
}
static void ProfileMathMax()
{
int maxValue;
Stopwatch stopwatch = new Stopwatch();
stopwatch.Start();
for (int j = 0; j < OuterLoopCount; j++)
{
maxValue = 0;
for (int i = 0; i < InnerLoopCount; i++)
{
maxValue = Math.Max(values[i], maxValue);
total += values[i];
}
}
stopwatch.Stop();
Console.WriteLine("Math.Max(a, max) - " + stopwatch.Elapsed.TotalSeconds + " seconds");
Console.WriteLine("Relative: " + (stopwatch.Elapsed.TotalSeconds - controlTotalSeconds) + " seconds");
}
static void ProfileMathMaxReverse()
{
int maxValue;
Stopwatch stopwatch = new Stopwatch();
stopwatch.Start();
for (int j = 0; j < OuterLoopCount; j++)
{
maxValue = 0;
for (int i = 0; i < InnerLoopCount; i++)
{
maxValue = Math.Max(maxValue, values[i]);
total += values[i];
}
}
stopwatch.Stop();
Console.WriteLine("Math.Max(max, a) - " + stopwatch.Elapsed.TotalSeconds + " seconds");
Console.WriteLine("Relative: " + (stopwatch.Elapsed.TotalSeconds - controlTotalSeconds) + " seconds");
}
static void ProfileInline()
{
int maxValue = 0;
Stopwatch stopwatch = new Stopwatch();
stopwatch.Start();
for (int j = 0; j < OuterLoopCount; j++)
{
maxValue = 0;
for (int i = 0; i < InnerLoopCount; i++)
{
maxValue = maxValue > values[i] ? values[i] : maxValue;
total += values[i];
}
}
stopwatch.Stop();
Console.WriteLine("max = max > a ? a : max: " + stopwatch.Elapsed.TotalSeconds + " seconds");
Console.WriteLine("Relative: " + (stopwatch.Elapsed.TotalSeconds - controlTotalSeconds) + " seconds");
}
static void ProfileIf()
{
int maxValue = 0;
Stopwatch stopwatch = new Stopwatch();
stopwatch.Start();
for (int j = 0; j < OuterLoopCount; j++)
{
maxValue = 0;
for (int i = 0; i < InnerLoopCount; i++)
{
if (values[i] > maxValue)
maxValue = values[i];
total += values[i];
}
}
stopwatch.Stop();
Console.WriteLine("if (a > max) max = a: " + stopwatch.Elapsed.TotalSeconds + " seconds");
Console.WriteLine("Relative: " + (stopwatch.Elapsed.TotalSeconds - controlTotalSeconds) + " seconds");
}
static void Main(string[] args)
{
Random rnd = new Random();
for (int i = 0; i < InnerLoopCount; i++)
{
//values[i] = i; // worst case: every new number biggest than the previous
//values[i] = i == 0 ? 1 : 0; // best case: first number is the maximum
values[i] = rnd.Next(int.MaxValue); // average case: random numbers
}
ProfileBase();
Console.WriteLine();
ProfileMathMax();
Console.WriteLine();
ProfileMathMaxReverse();
Console.WriteLine();
ProfileInline();
Console.WriteLine();
ProfileIf();
Console.ReadLine();
}
}
}
Estaba trabajando en un proyecto hoy y me encontré usando Math.Max en varios lugares e instrucciones en línea en otros lugares. Entonces, me preguntaba si alguien sabía cuál es "mejor" ... o, mejor dicho, cuáles son las diferencias reales.
Por ejemplo, en lo siguiente, c1 = c2
:
Random rand = new Random();
int a = rand.next(0,10000);
int b = rand.next(0,10000);
int c1 = Math.Max(a, b);
int c2 = a>b ? a : b;
Estoy preguntando específicamente sobre C #, pero supongo que la respuesta podría ser diferente en diferentes idiomas, aunque no estoy seguro de cuáles tienen conceptos similares.
Matemáticas.Max (a, b)
¿ NO es equivalente a a > b ? a : b
a > b ? a : b
en todos los casos.
Math.Max
devuelve el mayor valor de los dos argumentos, es decir:
if (a == b) return a; // or b, doesn''t matter since they''re identical
else if (a > b && b < a) return a;
else if (b > a && a < b) return b;
else return undefined;
El indefinido se asigna a double.NaN
en el caso de la sobrecarga doble de Math.Max
por ejemplo.
a> b? a: b
evalúa a a si a es mayor que b, lo que no significa necesariamente que b sea menor que a.
Un ejemplo simple que demuestra que no son equivalentes:
var a = 0.0/0.0; // or double.NaN
var b = 1.0;
a > b ? a : b // evaluates to 1.0
Math.Max(a, b) // returns double.NaN
Diría que es más rápido entender lo que hace Math.Max, y ese debería ser el único factor decisivo aquí.
Pero como indulgencia, es interesante considerar que Math.Max(a,b)
evalúa los argumentos una vez, mientras que a > b ? a : b
a > b ? a : b
evalúa uno de ellos dos veces. No es un problema con las variables locales, pero para las propiedades con efectos secundarios, el efecto secundario puede ocurrir dos veces.
En lo que respecta al rendimiento, las CPU modernas tienen un canal de comandos interno, de modo que cada comando de ensamblaje se ejecuta en varios pasos internos. (por ejemplo, captación, interpretación, cálculo, almacenamiento)
En la mayoría de los casos, la CPU es lo suficientemente inteligente como para ejecutar estos pasos en paralelo para comandos secuenciales, por lo que el rendimiento general es muy alto.
Esto está bien hasta que llega una rama ( if
, ?:
Etc.). La rama puede romper la secuencia y forzar a la CPU a desechar la tubería. Esto cuesta muchos ciclos de reloj.
En teoría, si el compilador es lo suficientemente inteligente, Math.Max
se puede implementar utilizando un comando de CPU integrado y se puede evitar la bifurcación.
En este caso, Math.Max
sería más rápido que el if
, pero depende del compilador.
En el caso de un trabajo de tipo Max más complicado en vectores, double []v; v.Max()
double []v; v.Max()
el compilador puede utilizar código de biblioteca altamente optimizado, que puede ser mucho más rápido que el código compilado normal.
Por lo tanto, es mejor utilizar Math.Max, pero también se recomienda verificar el compilador y el sistema de destino en particular si es lo suficientemente importante.
Pensé que sería divertido incluir algunos números en esta discusión, así que escribí un código para perfilarlo. Como era de esperar, son casi idénticos para todos los propósitos prácticos.
El código hace mil millones de bucles (sip 1 mil millones). Resta la sobrecarga del bucle que obtienes:
- Math.Max () tardó 0,0044 segundos en ejecutarse 1 billón de veces
- El en línea si tomó .0055 segundos para ejecutar 1 mil millones de veces
Reste la sobrecarga que calculé ejecutando un bucle vacío 1 billón de veces, la sobrecarga fue de 1.2 segundos.
Ejecuté esto en una computadora portátil, Windows 7 de 64 bits, 1.3 Ghz Intel Core i5 (U470). El código se compiló en modo de lanzamiento y se ejecutó sin un depurador adjunto.
Aquí está el código:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Diagnostics;
namespace TestMathMax {
class Program {
static int Main(string[] args) {
var num1 = 10;
var num2 = 100;
var maxValue = 0;
var LoopCount = 1000000000;
double controlTotalSeconds;
{
var stopwatch = new Stopwatch();
stopwatch.Start();
for (var i = 0; i < LoopCount; i++) {
// do nothing
}
stopwatch.Stop();
controlTotalSeconds = stopwatch.Elapsed.TotalSeconds;
Console.WriteLine("Control - Empty Loop - " + controlTotalSeconds + " seconds");
}
Console.WriteLine();
{
var stopwatch = new Stopwatch();
stopwatch.Start();
for (int i = 0; i < LoopCount; i++) {
maxValue = Math.Max(num1, num2);
}
stopwatch.Stop();
Console.WriteLine("Math.Max() - " + stopwatch.Elapsed.TotalSeconds + " seconds");
Console.WriteLine("Relative: " + (stopwatch.Elapsed.TotalSeconds - controlTotalSeconds) + " seconds");
}
Console.WriteLine();
{
var stopwatch = new Stopwatch();
stopwatch.Start();
for (int i = 0; i < LoopCount; i++) {
maxValue = num1 > num2 ? num1 : num2;
}
stopwatch.Stop();
Console.WriteLine("Inline Max: " + stopwatch.Elapsed.TotalSeconds + " seconds");
Console.WriteLine("Relative: " + (stopwatch.Elapsed.TotalSeconds - controlTotalSeconds) + " seconds");
}
Console.ReadLine();
return maxValue;
}
}
}
Resultados ACTUALIZADOS 2/7/2015
En un Windows 8.1, Surface 3 Pro, i7 4650U 2.3Ghz Funcionó como una aplicación de consola en modo de lanzamiento sin el depurador conectado.
- Math.Max () - 0.3194749 segundos
- Máx. En línea: 0.3465041 segundos
Si el JITer elige alinear la función Math.Max, el código ejecutable será idéntico a la instrucción if. Si Math.Max no está en línea, se ejecutará como una llamada de función con llamada y devolverá los gastos generales que no están presentes en la instrucción if. Por lo tanto, la instrucción if dará un rendimiento idéntico a Math.Max () en el caso interno o la instrucción if puede ser unos cuantos ciclos de reloj más rápido en el caso no integrado, pero la diferencia no se notará a menos que esté ejecutando decenas de millones de comparaciones.
Dado que la diferencia de rendimiento entre los dos es lo suficientemente pequeña como para ser despreciable en la mayoría de las situaciones, prefiero el Math.Max (a, b) porque es más fácil de leer.
Una de las principales diferencias que notaría de inmediato sería la legibilidad, por lo que sé por implementación / rendimiento, serían casi equivalentes.
Math.Max(a,b)
es muy fácil de entender, independientemente de los conocimientos de codificación anteriores.
a>b ? a : b
a>b ? a : b
requeriría que el usuario tenga algún conocimiento del operador ternario, al menos.
" Cuando tengas dudas, ve por la legibilidad "