O operador ternário é duas vezes mais lento que um bloco if-else?

246

Eu li todos os lugares que operador ternário é suposto ser mais rápido do que, ou pelo menos o mesmo que, o seu equivalente if- elsebloco.

No entanto, fiz o seguinte teste e descobri que não é o 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());

Meu computador levou 85 ms para executar o código acima. Mas se eu comentar o pedaço if- elsee descomentar a linha do operador ternário, levará cerca de 157 ms.

Por que isso está acontecendo?

user1032613
fonte
96
Primeira coisa a corrigir: não use DateTimepara medir o desempenho. Use Stopwatch. Em seguida, tempo um pouco mais longo - é um tempo muito curto para medir.
Jon Skeet
49
Use uma semente ao criar o Randomobjeto, para que ele sempre dê a mesma sequência. Se você testar código diferente com dados diferentes, poderá ver muito bem as diferenças no desempenho.
Guffa
12
Você também tentou compilá-lo / executá-lo no modo de liberação com as otimizações do compilador ativadas e sem o depurador conectado?
Chris Sinclair
7
@ LarryOBrien: Tomada interessante. Acabei de fazer um teste rápido do LINQPad e obtive resultados muito diferentes com a matriz classificada ou não. De fato, com ele classificado, reproduzo a mesma diferença de velocidade relatada. A remoção da classificação também remove a diferença de horário.
Chris Sinclair
39
O ponto aqui é que as microoptimizações de teste de desempenho são difíceis . Praticamente todas as coisas que você está observando em seu resultado estão relacionadas a erros no seu código de teste, não a diferenças no código significativo. Quando você consertar os listados aqui, haverá mais, posso garantir. A moral da história, não se preocupe com microoptimizações ou tentando testá-las em primeiro lugar. Se o código é realmente difícil de medir, significa que não é lento o suficiente para ser um gargalo; ignore isto.
Servy

Respostas:

376

Para responder a essa pergunta, examinaremos o código de montagem produzido pelos XIT e X64 JITs para cada um desses casos.

X86, se / então

    32:                 foreach (int i in array)
0000007c 33 D2                xor         edx,edx 
0000007e 83 7E 04 00          cmp         dword ptr [esi+4],0 
00000082 7E 1C                jle         000000A0 
00000084 8B 44 96 08          mov         eax,dword ptr [esi+edx*4+8] 
    33:                 {
    34:                     if (i > 0)
00000088 85 C0                test        eax,eax 
0000008a 7E 08                jle         00000094 
    35:                     {
    36:                         value += 2;
0000008c 83 C3 02             add         ebx,2 
0000008f 83 D7 00             adc         edi,0 
00000092 EB 06                jmp         0000009A 
    37:                     }
    38:                     else
    39:                     {
    40:                         value += 3;
00000094 83 C3 03             add         ebx,3 
00000097 83 D7 00             adc         edi,0 
0000009a 42                   inc         edx 
    32:                 foreach (int i in array)
0000009b 39 56 04             cmp         dword ptr [esi+4],edx 
0000009e 7F E4                jg          00000084 
    30:             for (int x = 0; x < iterations; x++)
000000a0 41                   inc         ecx 
000000a1 3B 4D F0             cmp         ecx,dword ptr [ebp-10h] 
000000a4 7C D6                jl          0000007C 

X86, ternário

    59:                 foreach (int i in array)
00000075 33 F6                xor         esi,esi 
00000077 83 7F 04 00          cmp         dword ptr [edi+4],0 
0000007b 7E 2D                jle         000000AA 
0000007d 8B 44 B7 08          mov         eax,dword ptr [edi+esi*4+8] 
    60:                 {
    61:                     value += i > 0 ? 2 : 3;
00000081 85 C0                test        eax,eax 
00000083 7F 07                jg          0000008C 
00000085 BA 03 00 00 00       mov         edx,3 
0000008a EB 05                jmp         00000091 
0000008c BA 02 00 00 00       mov         edx,2 
00000091 8B C3                mov         eax,ebx 
00000093 8B 4D EC             mov         ecx,dword ptr [ebp-14h] 
00000096 8B DA                mov         ebx,edx 
00000098 C1 FB 1F             sar         ebx,1Fh 
0000009b 03 C2                add         eax,edx 
0000009d 13 CB                adc         ecx,ebx 
0000009f 89 4D EC             mov         dword ptr [ebp-14h],ecx 
000000a2 8B D8                mov         ebx,eax 
000000a4 46                   inc         esi 
    59:                 foreach (int i in array)
000000a5 39 77 04             cmp         dword ptr [edi+4],esi 
000000a8 7F D3                jg          0000007D 
    57:             for (int x = 0; x < iterations; x++)
000000aa FF 45 E4             inc         dword ptr [ebp-1Ch] 
000000ad 8B 45 E4             mov         eax,dword ptr [ebp-1Ch] 
000000b0 3B 45 F0             cmp         eax,dword ptr [ebp-10h] 
000000b3 7C C0                jl          00000075 

X64, se / então

    32:                 foreach (int i in array)
00000059 4C 8B 4F 08          mov         r9,qword ptr [rdi+8] 
0000005d 0F 1F 00             nop         dword ptr [rax] 
00000060 45 85 C9             test        r9d,r9d 
00000063 7E 2B                jle         0000000000000090 
00000065 33 D2                xor         edx,edx 
00000067 45 33 C0             xor         r8d,r8d 
0000006a 4C 8B 57 08          mov         r10,qword ptr [rdi+8] 
0000006e 66 90                xchg        ax,ax 
00000070 42 8B 44 07 10       mov         eax,dword ptr [rdi+r8+10h] 
    33:                 {
    34:                     if (i > 0)
00000075 85 C0                test        eax,eax 
00000077 7E 07                jle         0000000000000080 
    35:                     {
    36:                         value += 2;
00000079 48 83 C5 02          add         rbp,2 
0000007d EB 05                jmp         0000000000000084 
0000007f 90                   nop 
    37:                     }
    38:                     else
    39:                     {
    40:                         value += 3;
00000080 48 83 C5 03          add         rbp,3 
00000084 FF C2                inc         edx 
00000086 49 83 C0 04          add         r8,4 
    32:                 foreach (int i in array)
0000008a 41 3B D2             cmp         edx,r10d 
0000008d 7C E1                jl          0000000000000070 
0000008f 90                   nop 
    30:             for (int x = 0; x < iterations; x++)
00000090 FF C1                inc         ecx 
00000092 41 3B CC             cmp         ecx,r12d 
00000095 7C C9                jl          0000000000000060 

X64, ternário

    59:                 foreach (int i in array)
00000044 4C 8B 4F 08          mov         r9,qword ptr [rdi+8] 
00000048 45 85 C9             test        r9d,r9d 
0000004b 7E 2F                jle         000000000000007C 
0000004d 45 33 C0             xor         r8d,r8d 
00000050 33 D2                xor         edx,edx 
00000052 4C 8B 57 08          mov         r10,qword ptr [rdi+8] 
00000056 8B 44 17 10          mov         eax,dword ptr [rdi+rdx+10h] 
    60:                 {
    61:                     value += i > 0 ? 2 : 3;
0000005a 85 C0                test        eax,eax 
0000005c 7F 07                jg          0000000000000065 
0000005e B8 03 00 00 00       mov         eax,3 
00000063 EB 05                jmp         000000000000006A 
00000065 B8 02 00 00 00       mov         eax,2 
0000006a 48 63 C0             movsxd      rax,eax 
0000006d 4C 03 E0             add         r12,rax 
00000070 41 FF C0             inc         r8d 
00000073 48 83 C2 04          add         rdx,4 
    59:                 foreach (int i in array)
00000077 45 3B C2             cmp         r8d,r10d 
0000007a 7C DA                jl          0000000000000056 
    57:             for (int x = 0; x < iterations; x++)
0000007c FF C1                inc         ecx 
0000007e 3B CD                cmp         ecx,ebp 
00000080 7C C6                jl          0000000000000048 

Primeiro: por que o código X86 é muito mais lento que o X64?

Isso ocorre devido às seguintes características do código:

  1. O X64 possui vários registros adicionais disponíveis e cada registro é de 64 bits. Isso permite que o X64 JIT execute o loop interno inteiramente usando registros além do carregamento ida matriz, enquanto o X86 JIT coloca várias operações de pilha (acesso à memória) no loop.
  2. valueé um número inteiro de 64 bits, que requer 2 instruções de máquina no X86 ( addseguidas por adc), mas apenas 1 no X64 ( add).

Segundo: por que o operador ternário é mais lento no X86 e no X64?

Isso ocorre devido a uma diferença sutil na ordem das operações que afetam o otimizador do JIT. Para JIT o operador ternário, em vez de codificar diretamente 2e 3nas addpróprias instruções da máquina, o JIT criando uma variável intermediária (em um registro) para conter o resultado. Esse registro é então estendido de sinal de 32 bits para 64 bits antes de adicioná-lo value. Como tudo isso é realizado nos registros do X64, apesar do aumento significativo da complexidade do operador ternário, o impacto líquido é um pouco minimizado.

O X86 JIT, por outro lado, é impactado em maior medida porque a adição de um novo valor intermediário no loop interno faz com que ele "derrame" outro valor, resultando em pelo menos 2 acessos adicionais à memória no loop interno (consulte os acessos para [ebp-14h]no código ternário X86).

Sam Harwell
fonte
18
O compilador também pode expandir o ternário para um if-else.
Dezfowler 26/06
13
Observe que o x86 é apenas mais lento ao usar o ternário - é tão rápido quanto o x64 ao usar o if / else . Portanto, a pergunta a ser respondida é: "por que o código X86 é muito mais lento que o X64 ao usar o operador ternário?".
Eren Ersönmez
18
Certamente não há uma boa razão para isso e a MS deve "consertar" isso - pois o Ternary é efetivamente apenas uma sintaxe mais curta para if / else ?! Você certamente não esperaria pagar uma penalidade de desempenho de qualquer maneira.
Niico 27/06
6
@niico Não há nada para 'consertar' no operador ternário. seu uso nesse caso só causa uma alocação de registro diferente. Em um caso diferente, pode ser mais rápido do que se / outro, como tentei explicar na minha resposta.
Eren Ersönmez
6
@ ErenErsönmez: Claro que há algo para corrigir. A equipe do otimizador pode analisar cuidadosamente os dois casos e encontrar uma maneira de fazer com que o operador ternário seja, nesse caso, tão rápido quanto o resto. Obviamente, essa correção pode ser inviável ou muito cara.
Brian
63

EDIT: Todas as alterações ... veja abaixo.

Eu não posso reproduzir seus resultados no CLR x64, mas eu puder em x86. No x64, vejo uma pequena diferença (menos de 10%) entre o operador condicional e o if / else, mas é muito menor do que você está vendo.

Fiz as seguintes alterações em potencial:

  • Executar em um aplicativo de console
  • Construa com /o+ /debug-e execute fora do depurador
  • Execute os dois trechos de código uma vez para JIT-los e, muitas vezes, para obter mais precisão
  • Usar Stopwatch

Resultados com /platform:x64(sem as linhas "ignorar"):

if/else with 1 iterations: 17ms
conditional with 1 iterations: 19ms
if/else with 1000 iterations: 17875ms
conditional with 1000 iterations: 19089ms

Resultados com /platform:x86(sem as linhas "ignorar"):

if/else with 1 iterations: 18ms
conditional with 1 iterations: 49ms
if/else with 1000 iterations: 17901ms
conditional with 1000 iterations: 47710ms

Detalhes do meu sistema:

  • CPU x64 i7-2720QM a 2.20GHz
  • Windows 8 de 64 bits
  • .NET 4.5

Então, ao contrário de antes, acho que você está vendo uma diferença real - e tudo tem a ver com o x86 JIT. Eu não gostaria de dizer exatamente o que está causando a diferença - posso atualizar o post mais tarde com mais detalhes, se eu puder me preocupar em entrar no cordbg :)

Curiosamente, sem classificar a matriz primeiro, termino com testes que demoram cerca de 4,5x, pelo menos em x64. Meu palpite é que isso tem a ver com previsão de ramificação.

Código:

using System;
using System.Diagnostics;

class Test
{
    static void Main()
    {
        Random r = new Random(0);
        int[] array = new int[20000000];
        for(int i = 0; i < array.Length; i++)
        {
            array[i] = r.Next(int.MinValue, int.MaxValue);
        }
        Array.Sort(array);
        // JIT everything...
        RunIfElse(array, 1);
        RunConditional(array, 1);
        // Now really time it
        RunIfElse(array, 1000);
        RunConditional(array, 1000);
    }

    static void RunIfElse(int[] array, int iterations)
    {        
        long value = 0;
        Stopwatch sw = Stopwatch.StartNew();

        for (int x = 0; x < iterations; x++)
        {
            foreach (int i in array)
            {
                if (i > 0)
                {
                    value += 2;
                }
                else
                {
                    value += 3;
                }
            }
        }
        sw.Stop();
        Console.WriteLine("if/else with {0} iterations: {1}ms",
                          iterations,
                          sw.ElapsedMilliseconds);
        // Just to avoid optimizing everything away
        Console.WriteLine("Value (ignore): {0}", value);
    }

    static void RunConditional(int[] array, int iterations)
    {        
        long value = 0;
        Stopwatch sw = Stopwatch.StartNew();

        for (int x = 0; x < iterations; x++)
        {
            foreach (int i in array)
            {
                value += i > 0 ? 2 : 3;
            }
        }
        sw.Stop();
        Console.WriteLine("conditional with {0} iterations: {1}ms",
                          iterations,
                          sw.ElapsedMilliseconds);
        // Just to avoid optimizing everything away
        Console.WriteLine("Value (ignore): {0}", value);
    }
}
Jon Skeet
fonte
31
Portanto, a pergunta que todo mundo ainda quer saber é por que há uma pequena diferença.
Brad M
1
@BradM: Bem, a IL será diferente, e qualquer diferença pode fazer todo tipo de coisa no momento em que é compilada por JIT e, em seguida, a própria CPU fez coisas desagradáveis.
precisa
4
@JonSkeet FYI. executou seu código, exatamente como você explicou. 19s vs 52s em x86 e 19s vs 21s em x64.
Eren Ersönmez
5
@ user1032613: Agora posso reproduzir seus resultados. Veja minha edição. Desculpas para duvidando de você antes - é incrível a diferença uma mudança na arquitetura pode fazer ...
Jon Skeet
3
@ BЈовић: De fato. Começou como não sendo capaz de reproduzi-lo, mas evoluiu com o tempo. Isso não explica o motivo, mas achei que ainda era uma informação útil (por exemplo, a diferença x64 x x86), e foi por isso que deixei de lado.
precisa
43

A diferença realmente não tem muito a ver com if / else vs ternário.

Olhando para as desmontagens instaladas (não vou me refazer aqui, veja a resposta do @ 280Z28), acontece que você está comparando maçãs e laranjas . Em um caso, você cria duas +=operações diferentes com valores constantes e a que você escolhe depende de uma condição e, no outro caso, cria um +=onde o valor a ser adicionado depende de uma condição.

Se você realmente deseja comparar if / else e ternário, seria uma comparação mais justa (agora ambos serão igualmente "lentos", ou podemos até dizer que o ternário é um pouco mais rápido):

int diff;
if (i > 0) 
    diff = 2;
else 
    diff = 3;
value += diff;

vs.

value += i > 0 ? 2 : 3;

Agora, a desmontagem do tornado if/elseé mostrada abaixo. Observe que isso é um pouco pior que o caso ternário, pois parou de usar os registradores para a variável de loop ( i) também.

                if (i > 0)
0000009d  cmp         dword ptr [ebp-20h],0 
000000a1  jle         000000AD 
                {
                    diff = 2;
000000a3  mov         dword ptr [ebp-24h],2 
000000aa  nop 
000000ab  jmp         000000B4 
                }
                else
                {
                    diff = 3;
000000ad  mov         dword ptr [ebp-24h],3 
                }
                value += diff;
000000b4  mov         eax,dword ptr [ebp-18h] 
000000b7  mov         edx,dword ptr [ebp-14h] 
000000ba  mov         ecx,dword ptr [ebp-24h] 
000000bd  mov         ebx,ecx 
000000bf  sar         ebx,1Fh 
000000c2  add         eax,ecx 
000000c4  adc         edx,ebx 
000000c6  mov         dword ptr [ebp-18h],eax 
000000c9  mov         dword ptr [ebp-14h],edx 
000000cc  inc         dword ptr [ebp-28h] 
Eren Ersönmez
fonte
5
Que tal enfatizar a comparação de maçãs e laranjas ?
Ken Kin
6
Bem, eu não diria que está comparando maçãs e laranjas. As duas variantes têm a mesma semântica ; portanto, o otimizador pode tentar as duas variantes de otimização e escolher o que for mais eficiente nos dois casos.
Vlad
Fiz o teste como você sugeriu: introduziu outra variável diff, mas o ternário ainda é muito mais lento - nem um pouco o que você disse. Você fez o experimento antes de postar esta "resposta"?
user1032613
9

Editar:

Adicionado um exemplo que pode ser feito com a instrução if-else, mas não com o operador condicional.


Antes da resposta, consulte [O que é mais rápido? ] no blog do Sr. Lippert. E acho que a resposta do Sr. Ersönmez é a mais correta aqui.

Estou tentando mencionar algo que devemos ter em mente com uma linguagem de programação de alto nível.

Primeiro, nunca ouvi dizer que o operador condicional deveria ser mais rápido ou ter o mesmo desempenho da instrução if-else em C♯ .

O motivo é simples: e se não houver operação com a instrução if-else:

if (i > 0)
{
    value += 2;
}
else
{
}

O requisito do operador condicional é que deve haver um valor para ambos os lados e, em C♯, também exige que ambos os lados :tenham o mesmo tipo. Isso apenas o torna diferente da instrução if-else. Assim, sua pergunta se torna uma pergunta perguntando como a instrução do código da máquina é gerada para que a diferença de desempenho.

Com o operador condicional, semanticamente é:

Qualquer que seja a expressão avaliada, há um valor.

Mas com a declaração if-else:

Se a expressão for avaliada como verdadeira, faça algo; caso contrário, faça outra coisa.

Um valor não está necessariamente envolvido com a instrução if-else. Sua suposição só é possível com a otimização.

Outro exemplo para demonstrar a diferença entre eles seria o seguinte:

var array1=new[] { 1, 2, 3 };
var array2=new[] { 5, 6, 7 };

if(i>0)
    array1[1]=4;
else
    array2[2]=4;

o código acima compila, no entanto, substitua a instrução if-else pelo operador condicional, que não compila:

var array1=new[] { 1, 2, 3 };
var array2=new[] { 5, 6, 7 };
(i>0?array1[1]:array2[2])=4; // incorrect usage 

O operador condicional e as instruções if-else são conceituais da mesma forma quando você faz a mesma coisa, possivelmente ainda mais rápido com o operador condicional em C , pois C está mais próximo da montagem da plataforma.


Para o código original que você forneceu, o operador condicional é usado em um loop foreach, o que atrapalha as coisas para ver a diferença entre eles. Então, estou propondo o seguinte código:

public static class TestClass {
    public static void TestConditionalOperator(int i) {
        long value=0;
        value+=i>0?2:3;
    }

    public static void TestIfElse(int i) {
        long value=0;

        if(i>0) {
            value+=2;
        }
        else {
            value+=3;
        }
    }

    public static void TestMethod() {
        TestConditionalOperator(0);
        TestIfElse(0);
    }
}

e a seguir estão duas versões do IL de otimizado e não. Como eles são longos, estou usando uma imagem para mostrar, o lado direito é o otimizado:

(Clique para ver a imagem em tamanho real.) hSN6s.png

Nas duas versões do código, a IL do operador condicional parece mais curta que a instrução if-else, e ainda há uma dúvida do código da máquina finalmente gerado. A seguir, são apresentadas as instruções dos dois métodos, e a imagem anterior não é otimizada; a última é a otimizada:

  • Instruções não otimizadas: (Clique para ver a imagem no tamanho original.) ybhgM.png

  • Instruções otimizadas: (Clique para ver a imagem no tamanho original.) 6kgzJ.png

Neste último, o bloco amarelo é o código executado somente se i<=0, e o bloco azul é quando i>0. Em qualquer versão das instruções, a instrução if-else é mais curta.

Observe que, para instruções diferentes, o [ CPI ] não é necessariamente o mesmo. Logicamente, para instruções idênticas, mais instruções custam um ciclo mais longo. Mas se o tempo de busca da instrução e o pipe / cache também foram levados em consideração, o tempo total real de execução depende do processador. O processador também pode prever as ramificações.

Os processadores modernos têm ainda mais núcleos; as coisas podem ser mais complexas com isso. Se você era um usuário de processador Intel, pode consultar o [ Manual de referência da otimização de arquiteturas Intel® 64 e IA-32 ].

Não sei se havia um CLR implementado por hardware, mas se sim, você provavelmente fica mais rápido com o operador condicional porque a IL é obviamente menor.

Nota: Todo o código da máquina é de x86.

Ken Kin
fonte
7

Fiz o que Jon Skeet fez e executei 1 iteração e 1.000 iterações e obtive um resultado diferente do OP e Jon. Na mina, o ternário é apenas um pouco mais rápido. Abaixo está o código exato:

static void runIfElse(int[] array, int iterations)
    {
        long value = 0;
        Stopwatch ifElse = new Stopwatch();
        ifElse.Start();
        for (int c = 0; c < iterations; c++)
        {
            foreach (int i in array)
            {
                if (i > 0)
                {
                    value += 2;
                }
                else
                {
                    value += 3;
                }
            }
        }
        ifElse.Stop();
        Console.WriteLine(String.Format("Elapsed time for If-Else: {0}", ifElse.Elapsed));
    }

    static void runTernary(int[] array, int iterations)
    {
        long value = 0;
        Stopwatch ternary = new Stopwatch();
        ternary.Start();
        for (int c = 0; c < iterations; c++)
        {
            foreach (int i in array)
            {
                value += i > 0 ? 2 : 3;
            }
        }
        ternary.Stop();


        Console.WriteLine(String.Format("Elapsed time for Ternary: {0}", ternary.Elapsed));
    }

    static void Main(string[] args)
    {
        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;

        runIfElse(array, 1);
        runTernary(array, 1);
        runIfElse(array, 1000);
        runTernary(array, 1000);
        
        Console.ReadLine();
    }

A saída do meu programa:

Tempo decorrido para If-Else: 00: 00: 00.0140543

Tempo decorrido para Ternário: 00: 00: 00.0136723

Tempo decorrido para If-Else: 00: 00: 14.0167870

Tempo decorrido para Ternário: 00: 00: 13.9418520

Outra execução em milissegundos:

Tempo decorrido para If-Else: 20

Tempo decorrido para Ternário: 19

Tempo decorrido para If-Else: 13854

Tempo decorrido para Ternário: 13610

Isso está sendo executado no XP de 64 bits e eu executei sem depuração.

Editar - Executando em x86:

Há uma grande diferença usando o x86. Isso foi feito sem a depuração na mesma máquina xp de 64 bits de antes, mas criado para as CPUs x86. Parece mais com os OPs.

Tempo decorrido para If-Else: 18

Tempo decorrido para Ternário: 35

Tempo decorrido para If-Else: 20512

Tempo decorrido para Ternário: 32673

Shaz
fonte
Você poderia experimentá-lo no x86? Obrigado.
user1032613
@ user1032613 Acho que pode haver uma grande diferença se você executar sem depurar vs com depuração.
precisa saber é o seguinte
@ user1032613 Acabei de editar minha postagem com dados do x86. Parece mais com o seu, onde o ternário é 2x mais lento.
Shaz
5

O código do assembler gerado contará a história:

a = (b > c) ? 1 : 0;

Gera:

mov  edx, DWORD PTR a[rip]
mov  eax, DWORD PTR b[rip]
cmp  edx, eax
setg al

Enquanto que:

if (a > b) printf("a");
else printf("b");

Gera:

mov edx, DWORD PTR a[rip]
mov eax, DWORD PTR b[rip]
cmp edx, eax
jle .L4
    ;printf a
jmp .L5
.L4:
    ;printf b
.L5:

Portanto, o ternário pode ser mais curto e mais rápido, simplesmente devido ao uso de menos instruções e sem saltos, se você estiver procurando por verdadeiro / falso. Se você usar valores diferentes de 1 e 0, receberá o mesmo código que um if / else, por exemplo:

a = (b > c) ? 2 : 3;

Gera:

mov edx, DWORD PTR b[rip]
mov eax, DWORD PTR c[rip]
cmp edx, eax
jle .L6
    mov eax, 2
jmp .L7
.L6:
    mov eax, 3
.L7:

Qual é o mesmo que o if / else.


fonte
4

Executar sem depurar ctrl + F5 parece que o depurador diminui significativamente o ifs e o ternário, mas parece que o operador ternário diminui muito mais.

Quando executo o seguinte código, aqui estão meus resultados. Eu acho que a pequena diferença de milissegundo é causada pelo compilador otimizando o max = max e removendo-o, mas provavelmente não está fazendo essa otimização para o operador ternário. Se alguém pudesse verificar a montagem e confirmar isso, seria incrível.

--Run #1--
Type   | Milliseconds
Ternary 706
If     704
%: .9972
--Run #2--
Type   | Milliseconds
Ternary 707
If     704
%: .9958
--Run #3--
Type   | Milliseconds
Ternary 706
If     704
%: .9972

Código

  for (int t = 1; t != 10; t++)
        {
            var s = new System.Diagnostics.Stopwatch();
            var r = new Random(123456789);   //r
            int[] randomSet = new int[1000]; //a
            for (int i = 0; i < 1000; i++)   //n
                randomSet[i] = r.Next();     //dom
            long _ternary = 0; //store
            long _if = 0;      //time
            int max = 0; //result
            s.Start();
            for (int q = 0; q < 1000000; q++)
            {
                for (int i = 0; i < 1000; i++)
                    max = max > randomSet[i] ? max : randomSet[i];
            }
            s.Stop();
            _ternary = s.ElapsedMilliseconds;
            max = 0;
            s = new System.Diagnostics.Stopwatch();
            s.Start();
            for (int q = 0; q < 1000000; q++)
            {
                for (int i = 0; i < 1000; i++)
                    if (max > randomSet[i])
                        max = max; // I think the compiler may remove this but not for the ternary causing the speed difference.
                    else
                        max = randomSet[i];
            }

            s.Stop();
            _if = s.ElapsedMilliseconds;
            Console.WriteLine("--Run #" + t+"--");
            Console.WriteLine("Type   | Milliseconds\nTernary {0}\nIf     {1}\n%: {2}", _ternary, _if,((decimal)_if/(decimal)_ternary).ToString("#.####"));
        }
CodeCamper
fonte
4

Observando a IL gerada, há 16 menos operações nela do que na instrução if / else (copiar e colar o código do @ JonSkeet). No entanto, isso não significa que deve ser um processo mais rápido!

Para resumir as diferenças na IL, o método if / else se traduz praticamente da mesma forma que o código C # lê (executando a adição dentro da ramificação), enquanto o código condicional carrega 2 ou 3 na pilha (dependendo do valor) e depois o adiciona ao valor fora do condicional.

A outra diferença é a instrução de ramificação usada. O método if / else usa um brtrue (branch if true) para pular a primeira condição e um branch incondicional para pular da primeira saída da instrução if. O código condicional usa um bgt (branch se maior que) em vez de um brtrue, o que poderia ser uma comparação mais lenta.

Além disso (depois de ler sobre a previsão de ramificação), pode haver uma penalidade de desempenho por ser menor. O ramo condicional possui apenas 1 instrução dentro do ramo, mas o if / else possui 7. Isso também explicaria por que há uma diferença entre usar long e int, porque alterar para int reduz o número de instruções nos ramos if / else em 1 (diminuindo a leitura antecipada)

Matthew Steeples
fonte
1

No código a seguir, se / else parece ser aproximadamente 1,4 vezes mais rápido que o operador ternário. No entanto, descobri que a introdução de uma variável temporária diminui o tempo de execução do operador ternário em aproximadamente 1,4 vezes:

If / Else: 98 ms

Ternário: 141 ms

Ternário com temp var: 100 ms

using System;
using System.Diagnostics;

namespace ConsoleApplicationTestIfElseVsTernaryOperator
{
    class Program
    {
        static void Main(string[] args)
        {
            Random r = new Random(0);
            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;
            Stopwatch stopwatch = new Stopwatch();

            value = 0;
            stopwatch.Restart();
            foreach (int i in array)
            {
                if (i > 0)
                {
                    value += 2;
                }
                else
                {
                    value += 3;
                }
                // 98 ms
            }
            stopwatch.Stop();
            Console.WriteLine("If/Else: " + stopwatch.ElapsedMilliseconds.ToString() + " ms");

            value = 0;
            stopwatch.Restart();
            foreach (int i in array)
            {
                value += (i > 0) ? 2 : 3; 
                // 141 ms
            }

            stopwatch.Stop();
            Console.WriteLine("Ternary: " + stopwatch.ElapsedMilliseconds.ToString() + " ms");

            value = 0;
            int tempVar = 0;
            stopwatch.Restart();
            foreach (int i in array)
            {
                tempVar = (i > 0) ? 2 : 3;
                value += tempVar; 
                // 100ms
            }
            stopwatch.Stop();
            Console.WriteLine("Ternary with temp var: " + stopwatch.ElapsedMilliseconds.ToString() + " ms");

            Console.ReadKey(true);
        }
    }
}
Alexey Novikov
fonte
0

Muitas respostas ótimas, mas achei algo interessante, mudanças muito simples causam impacto. Depois de fazer as alterações abaixo, para executar o operador if-else e ternário, levará o mesmo tempo.

em vez de escrever abaixo da linha

value +=  i > 0 ? 2 : 3;

Eu usei isso,

int a =  i > 0 ? 2 : 3;
value += a;

Uma das respostas abaixo também menciona que o que é uma maneira ruim de escrever operador ternário.

Espero que isso ajude você a escrever um operador ternário, em vez de pensar em qual é o melhor.

Operador ternário aninhado: Encontrei operador ternário aninhado e vários blocos if else também levarão o mesmo tempo para serem executados.

Ravindra Sinare
fonte