He leído todas partes que el operador ternario se supone que es más rápido que el, o al menos lo mismo que, su equivalente if
- else
bloque.
Sin embargo, hice la siguiente prueba y descubrí que no es el caso:
Random r = new Random();
int[] array = new int[20000000];
for(int i = 0; i < array.Length; i++)
{
array[i] = r.Next(int.MinValue, int.MaxValue);
}
Array.Sort(array);
long value = 0;
DateTime begin = DateTime.UtcNow;
foreach (int i in array)
{
if (i > 0)
{
value += 2;
}
else
{
value += 3;
}
// if-else block above takes on average 85 ms
// OR I can use a ternary operator:
// value += i > 0 ? 2 : 3; // takes 157 ms
}
DateTime end = DateTime.UtcNow;
MessageBox.Show("Measured time: " + (end-begin).TotalMilliseconds + " ms.\r\nResult = " + value.ToString());
Mi computadora tardó 85 ms en ejecutar el código anterior. Pero si comento el fragmento if
- else
y descomento la línea del operador ternario, tomará aproximadamente 157 ms.
¿Por qué está pasando esto?
c#
performance
conditional-operator
usuario1032613
fuente
fuente
DateTime
para medir el rendimiento. UsoStopwatch
. Luego, el tiempo es bastante más largo: es un tiempo muy corto para medir.Random
objeto, para que siempre tenga la misma secuencia. Si prueba un código diferente con datos diferentes, puede ver muy bien las diferencias en el rendimiento.Respuestas:
Para responder a esta pregunta, examinaremos el código de ensamblaje producido por los JIT X86 y X64 para cada uno de estos casos.
X86, si / entonces
X86, ternario
X64, si / entonces
X64, ternario
Primero: ¿por qué es el código X86? es mucho más lento que X64?
Esto se debe a las siguientes características del código:
i
desde la matriz, mientras que el X86 JIT coloca varias operaciones de pila (acceso a memoria) en el bucle.value
es un entero de 64 bits, que requiere 2 instrucciones de máquina en X86 (add
seguido deadc
) pero solo 1 en X64 (add
).Segundo: ¿por qué el operador ternario es más lento en X86 y X64?
Esto se debe a una sutil diferencia en el orden de las operaciones que afectan el optimizador del JIT. Para JIT el operador ternario, en lugar de codificar directamente
2
y3
en lasadd
instrucciones de la máquina, el JIT crea una variable intermedia (en un registro) para contener el resultado. Este registro se extiende luego de 32 bits a 64 bits antes de agregarlovalue
. Como todo esto se realiza en registros para X64, a pesar del aumento significativo en la complejidad para el operador ternario, el impacto neto se minimiza en cierta medida.El X86 JIT, por otro lado, se ve afectado en mayor medida porque la adición de un nuevo valor intermedio en el bucle interno hace que "derrame" otro valor, lo que resulta en al menos 2 accesos de memoria adicionales en el bucle interno (ver los accesos a
[ebp-14h]
en el código ternario X86).fuente
EDITAR: Todos los cambios ... ver más abajo.
No puedo reproducir sus resultados en el x64 CLR, pero puedo en x86. En x64 puedo ver un pequeño diferencia (menos del 10%) entre el operador condicional y el if / else, pero es mucho más pequeño de lo que estás viendo.
He realizado los siguientes cambios potenciales:
/o+ /debug-
y ejecutar fuera del depuradorStopwatch
Resultados con
/platform:x64
(sin las líneas "ignorar"):Resultados con
/platform:x86
(sin las líneas "ignorar"):Detalles de mi sistema:
Entonces, a diferencia de antes, creo que está viendo una diferencia real, y todo tiene que ver con el x86 JIT. No me gustaría decir exactamente qué está causando la diferencia: puedo actualizar la publicación más adelante con más detalles si puedo molestarme en entrar en cordbg :)
Curiosamente, sin ordenar primero la matriz, termino con pruebas que toman aproximadamente 4.5 veces más, al menos en x64. Mi conjetura es que esto tiene que ver con la predicción de rama.
Código:
fuente
La diferencia realmente no tiene mucho que ver con if / else vs ternary.
Mirando los desensamblados sacudidos (no voy a repetir aquí, por favor vea la respuesta de @ 280Z28), resulta que está comparando manzanas y naranjas . En un caso, crea dos
+=
operaciones diferentes con valores constantes y cuál elige depende de una condición, y en el otro caso, crea un valor+=
donde el valor a agregar depende de una condición.Si desea comparar realmente si / else vs ternary, esta sería una comparación más justa (ahora ambos serán igualmente "lentos", o incluso podríamos decir que ternary es un poco más rápido):
vs.
Ahora el desmontaje para el se
if/else
convierte en como se muestra a continuación. Tenga en cuenta que esto es un poco peor que el caso ternario, ya que también dejó de usar los registros para la variable de bucle (i
).fuente
diff
, pero el ternario sigue siendo mucho más lento, nada de lo que dijo. ¿Hiciste el experimento antes de publicar esta "respuesta"?Editar:
Se agregó un ejemplo que se puede hacer con la instrucción if-else pero no con el operador condicional.
Antes de la respuesta, eche un vistazo a [ ¿Cuál es más rápido? ] en el blog del Sr. Lippert. Y creo que la respuesta del Sr. Ersönmez es la más correcta aquí.
Estoy tratando de mencionar algo que debemos tener en cuenta con un lenguaje de programación de alto nivel.
En primer lugar, nunca escuché que el operador condicional se supone que sea más rápido o que tenga el mismo rendimiento con la instrucción if-else en C♯ .
La razón es simple: ¿qué pasa si no hay operación con la instrucción if-else?
El requisito del operador condicional es que debe haber un valor con cualquier lado, y en C♯ también requiere que ambos lados
:
tengan el mismo tipo. Esto solo lo hace diferente de la declaración if-else. Por lo tanto, su pregunta se convierte en una pregunta que pregunta cómo se genera la instrucción del código de máquina para que la diferencia de rendimiento.Con el operador condicional, semánticamente es:
Cualquiera que sea la expresión evaluada, hay un valor.
Pero con la declaración if-else:
Si la expresión se evalúa como verdadera, haga algo; si no, haz otra cosa.
Un valor no está necesariamente involucrado con la declaración if-else. Su suposición solo es posible con la optimización.
Otro ejemplo para demostrar la diferencia entre ellos sería el siguiente:
el código anterior compila, sin embargo, reemplace la instrucción if-else con el operador condicional simplemente no compilará:
El operador condicional y las declaraciones if-else son conceptuales iguales cuando hace lo mismo, posiblemente incluso más rápido con el operador condicional en C , ya que C está más cerca del ensamblaje de la plataforma.
Para el código original que proporcionó, el operador condicional se usa en un bucle foreach, lo que desordenaría las cosas para ver la diferencia entre ellos. Entonces propongo el siguiente código:
y las siguientes son dos versiones de la IL de optimizado y no. Como son largos, estoy usando una imagen para mostrar, el lado derecho es el optimizado:
En ambas versiones de código, la IL del operador condicional se ve más corta que la instrucción if-else, y todavía hay una duda sobre el código de máquina finalmente generado. Las siguientes son las instrucciones de ambos métodos, y la primera imagen no está optimizada, la segunda es la optimizada:
Instrucciones no optimizadas: (Haga clic para ver la imagen a tamaño completo).
Instrucciones optimizadas: (Haga clic para ver la imagen a tamaño completo).
En este último, el bloque amarillo es el código que solo se ejecuta si
i<=0
, y el bloque azul es cuandoi>0
. En cualquier versión de instrucciones, la instrucción if-else es más corta.Tenga en cuenta que, para diferentes instrucciones, el [ CPI ] no es necesariamente el mismo. Lógicamente, para la misma instrucción, más instrucciones cuestan un ciclo más largo. Pero si el tiempo de obtención de instrucciones y la canalización / caché también se tuvieron en cuenta, el tiempo total real de ejecución depende del procesador. El procesador también puede predecir las ramas.
Los procesadores modernos tienen incluso más núcleos, las cosas pueden ser más complejas con eso. Si usted fuera un usuario del procesador Intel, es posible que desee ver el [ Manual de referencia de optimización de arquitecturas Intel® 64 e IA-32 ].
No sé si hubo un CLR implementado en hardware, pero en caso afirmativo, es probable que sea más rápido con el operador condicional porque la IL es obviamente menor.
Nota: Todo el código de la máquina es de x86.
fuente
Hice lo que hizo Jon Skeet y ejecuté 1 iteración y 1,000 iteraciones y obtuve un resultado diferente de OP y Jon. En el mío, el ternario es solo un poco más rápido. A continuación se muestra el código exacto:
La salida de mi programa:
Otra carrera en milisegundos:
Esto se ejecuta en XP de 64 bits, y lo ejecuté sin depurar.
Editar - Ejecutando en x86:
Hay una gran diferencia con x86. Esto se hizo sin depurar en y en la misma máquina xp de 64 bits que antes, pero construida para CPU x86. Esto se parece más a los OP.
fuente
El código de ensamblador generado contará la historia:
Genera:
Mientras:
Genera:
Por lo tanto, el ternario puede ser más corto y más rápido simplemente debido al uso de menos instrucciones y sin saltos si está buscando verdadero / falso. Si utiliza valores distintos de 1 y 0, obtendrá el mismo código que un if / else, por ejemplo:
Genera:
Que es lo mismo que if / else.
fuente
Ejecutar sin depurar ctrl + F5 parece que el depurador ralentiza significativamente tanto ifs como ternary pero parece que ralentiza mucho más al operador ternary.
Cuando ejecuto el siguiente código aquí están mis resultados. Creo que la pequeña diferencia de milisegundos es causada por el compilador que optimiza max = max y lo elimina, pero probablemente no está haciendo esa optimización para el operador ternario. Si alguien pudiera verificar el ensamblaje y confirmar esto, sería increíble.
Código
fuente
Mirando el IL generado, hay 16 operaciones menos en eso que en la declaración if / else (copiar y pegar el código de @ JonSkeet). Sin embargo, eso no significa que deba ser un proceso más rápido.
Para resumir las diferencias en IL, el método if / else se traduce en casi lo mismo que las lecturas del código C # (realizando la adición dentro de la rama), mientras que el código condicional carga 2 o 3 en la pila (dependiendo del valor) y luego lo agrega al valor fuera del condicional.
La otra diferencia es la instrucción de ramificación utilizada. El método if / else utiliza una brtrue (rama si es verdadera) para saltar sobre la primera condición, y una rama incondicional para saltar desde la primera salida de la instrucción if. El código condicional usa un bgt (rama si es mayor que) en lugar de un brtrue, que posiblemente podría ser una comparación más lenta.
Además (después de leer acerca de la predicción de sucursal), puede haber una penalización de rendimiento para la sucursal más pequeña. La rama condicional solo tiene 1 instrucción dentro de la rama pero el if / else tiene 7. Esto también explicaría por qué hay una diferencia entre usar long e int, porque cambiar a int reduce el número de instrucciones en las ramas if / else en 1 (haciendo menos la lectura anticipada)
fuente
En el siguiente código, if / else parece ser aproximadamente 1,4 veces más rápido que el operador ternario. Sin embargo, descubrí que la introducción de una variable temporal disminuye el tiempo de ejecución del operador ternario aproximadamente 1,4 veces:
fuente
Demasiadas respuestas geniales, pero encontré algo interesante, cambios muy simples causan impacto. Después de realizar el cambio a continuación, ejecutar if-else y el operador ternario llevará el mismo tiempo.
en lugar de escribir debajo de la línea
Yo usé esto
Una de las respuestas a continuación también menciona que lo que es una mala forma de escribir operador ternario.
Espero que esto te ayude a escribir el operador ternario, en lugar de pensar cuál es mejor.
Operador ternario anidado : Encontré un operador ternario anidado y un bloque múltiple si no, también tardará el mismo tiempo en ejecutarse.
fuente