Тринарний оператор вдвічі повільніше, ніж у блоці if-else?

Я скрізь читаю, що тернарний оператор повинен бути швидшим за, або, принаймні, таким же, як його еквівалентний блок if - else .

Однак я зробив наступний тест і дізнався, що це не так:

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());

Мій комп'ютер зайняв 85 мсек, щоб запустити код вище. Але якщо я коментую фрагмент if - else і розкоментую трійковий оператор, це займе близько 157 мсек.

Чому це відбувається?

234
Спробуйте запустити його, використовуючи (i <0)? (Значення + = 2): (значення + = 3) .
додано Автор AJMansfield, джерело
додано Автор Erik Philips, джерело
Перше, що потрібно вирішити: не використовуйте DateTime для вимірювання продуктивності. Використовуйте Секундомір . Далі, час досить довгий - це дуже короткий час для вимірювання.
додано Автор Jon Skeet, джерело
Дуже добре буде те, що ви використовуєте консольний додаток, а не те, що виглядає як додаток WinForms ...
додано Автор Jon Skeet, джерело
FWIW, під Mono на Mac немає різниці: larryobrien $ csharp tnry.cs минуло 00: 00: 00.0637078 larryobrien $ csharp tnry.cs минуло 00: 00: 00.0631514
додано Автор Larry OBrien, джерело
Чому ви сортували свій масив? Які результати ви отримаєте, якщо цього не зробите?
додано Автор Larry OBrien, джерело
Я намагався зациклювати його ще 10 разів, з насінням 42, отримавши 849 мсек і 1564 мсек. Я не думаю, що це помилка на вимірювальну частину.
додано Автор user1032613, джерело
@Серви Подивіться на його відповідь Ларрі Обріену вище.
додано Автор RBarryYoung, джерело
Я сильно підозрюю, що це пов'язано з цією відповіддю тут: stackoverflow.com/q/11227809/109122 . (Це, мабуть, найвищий рейтинг питання та відповідь на SO.)
додано Автор RBarryYoung, джерело
@RBarryYoung Не думайте так. Це відсортований масив у тестах обох . Він ніколи не порівнює сортовані траси з несортованими трасами.
додано Автор Servy, джерело
Справа в тому, що тестування мікрооптимізацій на продуктивність важко . Практично всі речі, які ви спостерігаєте у вашому результаті, пов'язані з помилками у вашому тестовому коді, а не відмінності у змістовному коді. Коли ви виправите перераховані тут, буде більше, можу запевнити вас. Мораль розповіді, не турбуйтеся мікрооптимізацією чи намагаються перевірити їх в першу чергу. Якщо кодекс насправді важко виміряти, це означає, що це недостатньо повільно, щоб бути вузьким місцем; ігнорувати це.
додано Автор Servy, джерело
Використовуйте насіння, коли ви створюєте об'єкт Random , так що він завжди дає ту ж послідовність. Якщо ви перевіряєте різні коди з різними даними, ви можете добре бачити різницю в продуктивності.
додано Автор Guffa, джерело
Для вашого коду існує багато можливостей оптимізації компілятора, що вплине на його час виконання та перекочування результатів. (Масив сортується за одним, перевіряючи знак значення, завжди додаючи один із двох чисел)
додано Автор NominSim, джерело
@ ChrisSinclair На підставі відповіді Скета, причиною того, що в несортованому випадку немає різниці у часі, є те, що неправильні прогнози галузі настільки дорогі, що вони, ймовірно, приховують хвилинні відмінності.
додано Автор Mysticial, джерело
@ Відповідь 280Z28 є найкращим - не конкурс - але ви завжди повинні розглянути питання про ліквідацію необхідності умовного, коли мова йде про продуктивність. JavaScript є вільно набраним, тому ви можете змінити значення value = + = i> 0? 2: 3; до значення + = (i> 0) + 2; . Як щодо збирання для цього, @ 280Z28?
додано Автор thor2k, джерело
@LarryOBrien: Цікавий прийом. Я просто зробив тест LINQPad і отримав дуже різні результати, якщо масив було відсортовано чи ні. Фактично, з його сортуванням я відтворюю ту саму різницю швидкості повідомляється. Видалення сорту також видаляє різницю часу.
додано Автор Chris Sinclair, джерело
Ви також спробували скомпілювати/запустити його в режимі випуску з увімкненим оптимізацією компілятора, без додавання налагоджувача?
додано Автор Chris Sinclair, джерело

9 Відповіді

Щоб відповісти на це питання, ми розглянемо код збірки, створеного JITs X86 і X64 для кожного з цих випадків.

X86, якщо/тоді

    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, трійковий

    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, якщо/потім

    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, трійковий

    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 

По-перше: чому код X86 стільки повільний, ніж X64?

Це пов'язано з такими характеристиками коду:

  1. X64 has several additional registers available, and each register is 64-bits. This allows the X64 JIT to perform the inner loop entirely using registers aside from loading i from the array, while the X86 JIT places several stack operations (memory access) in the loop.
  2. value is a 64-bit integer, which requires 2 machine instructions on X86 (add followed by adc) but only 1 on X64 (add).

Другий: чому тернарний оператор працює повільніше як на X86, так і на X64?

Це пов'язано з тонкою різницею в послідовності операцій, що впливають на оптимізатор JIT. Для JIT трійкового оператора, а не безпосередньо кодування 2 і 3 в самих інструкціях машини add , JIT створює проміжну змінну (у зареєструйтесь), щоб провести результат. Цей реєстр потім розширюється з 32-біт на 64-біт, перш ніж додати його до value . Оскільки все це виконується в регістрах для X64, незважаючи на значне збільшення складності для трійкового оператора, чистий вплив трохи мінімізується.

Інструмент JIT X86, з іншого боку, впливає на більшу ступінь, оскільки додавання нового проміжного значення у внутрішній петлі змушує його "пролити" інше значення, в результаті чого принаймні 2 додаткових доступу до пам'яті у внутрішній петлі (див. Звернення до [ebp-14h] в трійчастий код X86).

368
додано
так що ви кажете, що тернарний вираз фактично вимагає зробити щось дещо інше, ніж якщо/тоді буде в цьому випадку? Замість того, щоб просто виконуватися інакше, щоб зробити щось ідентичне?
додано Автор niico, джерело
Звичайно, для цього немає ніяких підстав, і MS слід "виправити" це - як Ternary фактично просто короткий синтаксис, якщо/else ?! Ви, звичайно, не очікували сплатити штраф за виконання.
додано Автор niico, джерело
@dez Це не означає, що тут велика різниця, де value - це локальна змінна. Однак, якщо value будь-яким чином відображається для іншого потоку (наприклад, будучи полем класу), порядок операцій буде ключовим - у коді if/else i читається до value , але у випадку термінатора оператор value читається перед тим, як i .
додано Автор Sam Harwell, джерело
@quetzalcoatl Я використав відладчик VS, але явно не відмітив параметр "Пригнічувати оптимізацію JIT при завантаженні модуля"
додано Автор Sam Harwell, джерело
@KenKin: Я думаю, ви недооцінюєте вартість "простих" виправлень. "Оптимізований таким же способом" може бути не таким простим, як ви думаєте, і насправді іноді може призвести до погіршення продуктивності. Крім того, навіть невеликі зміни мають великі витрати через вимоги до розслідування та тестування. Чи не високі ці витрати теж , я не знаю, але, мабуть, існує величезна кількість подібних незначних пропозицій щодо оптимізації подібних до цього, багато з яких є помилковими, дублюються або мають витончені та невиразні проблеми.
додано Автор Brian, джерело
@ ErenErsönmez: Звичайно, є щось виправити. Команда оптимізатора може ретельно проаналізувати два випадки і знайти спосіб викликати тимчасовий оператор, в цьому випадку, так само швидко, як і раніше. Звичайно, такий виправлення може бути нездійсненним або занадто дорогим.
додано Автор Brian, джерело
Дякую за монтажні звалища. По суті, те, що я бачив, було дуже різним, оскільки трійковий op створював чотири строкових архівів для зберігання 64-бітових довжин, тоді як ifelse просто використовував регістри. Цікаво, чому результуюча збірка відрізняється. Ви отримали ваші звалища з VS Native debugger? Ви використовували ngen або будь-який інший аналогічний інструмент?
додано Автор quetzalcoatl, джерело
@ Брайан: Звичайно, якщо те, що ви маєте на увазі, - це не тільки рівність окремих випадків між цими двома. Що я хочу сказати, це принципово різні речі, і лише в якійсь мірі концептуально еквівалентні.
додано Автор Ken Kin, джерело
@ Брайан: Було б дорого, щоб визначити єдиний, якщо-else, збігається з тим, як люди думають, що умовний оператор нагадує, і тоді інструкції можна оптимізувати однаково. Який не не має сенсу , але перетворює сенс .
додано Автор Ken Kin, джерело
@niico немає нічого "виправити" про трійкового оператора. це використання в цьому випадку просто призводить до іншого розподілу реєстру. У іншому випадку це може бути швидше, ніж якщо/інакше, як я намагався пояснити у моїй відповіді.
додано Автор Eren Ersönmez, джерело
Зверніть увагу, що при використанні трійкового x86 працює лише повільніше - при використанні if/else це настільки ж стільки ж, скільки x64. Отже, відповідь на це питання: "Чому кодек X86 набагато повільніше, ніж X64, коли використовує трійковий оператор?".
додано Автор Eren Ersönmez, джерело
Компілятор може також розширити трійку в будь-який інший.
додано Автор dez, джерело

EDIT: усі зміни ... див. Нижче.

Я не можу відтворити ваші результати на CLR x64, але я може на x86. На x64 я бачу маленьку різницю (менше 10%) між умовним оператором і if/else, але це набагато менше, ніж ви бачите.

Я зробив такі можливі зміни:

  • Run in a console app
  • Build with /o+ /debug-, and run outside the debugger
  • Run both pieces of code once to JIT them, then lots of times for more accuracy
  • Use Stopwatch

Результати з /platform: x64 (без ліній "ігнорувати"):

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

Результати з /platform: x86 (без ліній "ігнорувати"):

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

Мої дані системи:

  • x64 i7-2720QM процесор @ 2.20 ГГц
  • 64-розрядний Windows 8
  • .NET 4.5

Тому, на відміну від попередніх, я думаю, що ви бачите реальну різницю - і це все залежить від JIT X86. Я б не хотів би точно сказати, що що викликає різницю - я можу оновити повідомлення пізніше з більш детальною інформацією, якщо я можу турбуватися, щоб піти в cordbg :)

Цікаво, що, перш ніж сортувати масив, я в кінцевому підсумку з тестами, які займають близько 4.5x довго, принаймні на x64. Я думаю, що це пов'язано з прогнозом галузі.

Код:

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);
    }
}
61
додано
Ваша відповідь лише пояснює результати, але насправді не відповідає на питання: чому це відбувається
додано Автор BЈовић, джерело
@Сервіс: Я не впевнений, що це шум - це, здається, досить повторюване, навіть якщо я перемикаю порядок проведення тестів.
додано Автор Jon Skeet, джерело
@ ErenErsönmez: Це було б цікаво ... Додавання блоку try/catch у цей код зразка, здається, сповільнює версію if/else до майже такої ж, як умовна операторська версія
додано Автор Jon Skeet, джерело
@BradM: Ну, ІЛ буде іншим, і будь-яка різниця взагалі могла б робити всі види речей до того часу, коли це JIT-скомпільований, а потім сам процесор зробив неприємні речі для нього.
додано Автор Jon Skeet, джерело
@ user1032613: тепер я може відтворити ваші результати. Див. Мою редакцію. Вибачте за те, що ви заперечували раніше - це вражає різницю, яку може зробити зміна в архітектурі ...
додано Автор Jon Skeet, джерело
@ user7116: версія "1 iteration" не є надзвичайно корисною, хоча - це 1000 ітерацій (або 100, якщо ви не бажаєте чекати як довго) версії, яка є більш важливою.
додано Автор Jon Skeet, джерело
@ ErenErsönmez: Цікаво - це спровокувало мене явно виділити x86 в моєму вікні, що потім спричинило зміну. Буде редагувати
додано Автор Jon Skeet, джерело
@ user1032613: Якщо ви хочете вирішити це, то добре - але я б не вірив у різницю, що мала, щоб дати щось дійсно значуще. Враховуючи, що я вже вказав код, щоб запустити його довше, чому б це не зробити?
додано Автор Jon Skeet, джерело
@ user1032613: Я включив дані моєї системи тут - цілком можуть бути дуже релевантні.
додано Автор Jon Skeet, джерело
@ user1032613: Ще одна точка зору: якщо ви приймаєте думку, що 20 мільйонів вже є чимось числом, я б сказав, що 156мс вже є невелика кількість часу для ітерації понад 20 мільйонів номерів. Я сильно підозрюю, що кількість програм, де ця петля буде вузьким місцем, дуже мала.
додано Автор Jon Skeet, джерело
@ kitzalcoatl: Випуск, відповідно до "/ o +/debug-" - Я встановлюю SDK для Windows, щоб я міг пірнути в JITted код.
додано Автор Jon Skeet, джерело
@ КенКін: Ні, там точно немає гарантії, - але я б хотів зрозуміти щось трохи краще :)
додано Автор Jon Skeet, джерело
@ Бейович: Дійсно. Це почалося з того, що він взагалі не міг відтворити його, але з часом розвивався. Це не дає причини, але я думав, що це все-таки корисна інформація (наприклад, різниця в x64-x86), тому я залишив це.
додано Автор Jon Skeet, джерело
@Jonskeet Я запущу ваш код пізніше сьогодні ввечері, коли я перейду до більш швидкого комп'ютера. Наразі з 1 ітерацією, використовуючи ваш незмінений код, я як і раніше отримую 85ms/156ms як мій результат. Я оновив свій результат пізніше, для науки.
додано Автор user1032613, джерело
@JonSkeet насправді я думаю, що 20 мільйонів вже досить великий, так що я б сказав, що біля 1 ітерації більш-менш достатньо ... Оглядаючи свій результат, 1 ітерація, здається, забезпечує таке саме співвідношення різниці, як 1000 ітерацій версія
додано Автор user1032613, джерело
Я запустив ваш код без змін, ось мій результат: 1 ітерація: 85 мсек, 156 мс.
додано Автор user1032613, джерело
@ BradM Шум у системі. У його машині працюють інші речі, процесор не буде мати стабільну продуктивність на 100%, бігатиме надходження GC, інші довільні фонові завдання, що відбуваються під час виконання (навіть якщо ви вимкнули кожен інший додаток на машині) тощо.
додано Автор Servy, джерело
Який біг спочатку здається ідентифікувати "переможця" після JIT.
додано Автор user7116, джерело
@JonSkeet: в кращому випадку з 5x1000 (використовуючи різні випадкові насіння, хоча), я отримав "поштовх" в тому, хто був швидше, на основі якого побіг першим.
додано Автор user7116, джерело
Тепер цікаво. Я думаю, що це буде ідеальним кандидатом для розбирання на остаточні двійкові файли і перевірити, що насправді що спроба/зловмин змінився в тому, якщо/інше .. Мені складно зобразити те, що може бути введено. Не навіть бокс. Ел. Прости мене за запитання. Налагодження чи випуск? У налагодженні я дійсно можу собі уявити додаткові чек ..
додано Автор quetzalcoatl, джерело
Ах, вибачте, не помітили, що перемикачі
додано Автор quetzalcoatl, джерело
На моєму x86 інший (26ms) створює близько 60b з (a? JbbbbbJcccccd) структурою, а трійна версія (60ms) генерує близько 90 байт машинного коду з структурою (aaaa? JbbbJcccddddddd) і використовує вдвічі більше пам'яті для локальних змінних. АА та ДД є частинами звичайного циклу,? J є умовною гілкою, яка вибирає B або C, а J безумовний, що стрибає до D. Кількість букв - це лише візуальне відображення довжини. Трійна готує значення за допомогою першого або другого набору місцевих жителів, потім виконує загальне додавання, ifelse використовує один набір місцевих організацій і має повний код, який дублюється.
додано Автор quetzalcoatl, джерело
Однак, спостерігаючи за монтажним кодом, я не знаю, чи правильно я створив оптимізовану версію .. Це виглядає .. настільки багатослівним, що я насправді сумніваюсь мені вдалося оптимізувати її, але час відповідає вашим спостереженням: трохи більше двох разів
додано Автор quetzalcoatl, джерело
.. і додавання крихітних обгортків цілими методами нічого не змінилося. Я не намагався додати його до тіла, я думаю, що це змінить ситуацію занадто багато. Я намагався зменшити значення long -> int , і обидва приклади працюють швидше: 23 і 26 мс замість 26 і 60 мс. Коди, створені як ifelse, так і трійковими, були набагато простішими через зміну типу даних, але загальна структура залишалася незмінною. Ну, вибачте за перевантаження коментарів, але мені цікаво почути від того, хто отримує версію "правильно оптимізованої" - чи вона виглядає схоже?
додано Автор quetzalcoatl, джерело
Все ще потрібна статистична перевірка, на знак, для науки
додано Автор Ast Derek, джерело
Отже, питання все ще вмирає, щоб знати, чому є навіть невелика різниця.
додано Автор Brad M, джерело
@JonSkeet: Я також подумав про цю відповідь, оскільки не хотів би точно сказати, що викликає різницю , оскільки я вірю, що не має відношення до мови. Я ніколи не чув, що умовний оператор гарантує бути рівною мірою або навіть швидше продуктивності за допомогою if-else в C#, чи не так?
додано Автор Ken Kin, джерело
@JonSkeet Якщо я зміню типи long на int у ваших методах, різниця продуктивності зникне. Тому я думаю, що це може бути пов'язано з проблемою випуску реєстру, яка була визначена тут: stackoverflow.com/q/8928403/201088 .
додано Автор Eren Ersönmez, джерело
@JonSkeet FYI. запустив ваш код, точно так, як ви пояснили. 19 і 52 у х86, а 19-ти та 21-х у х64-х.
додано Автор Eren Ersönmez, джерело
Також зауважте: я отримую ті ж результати в обох VS2010/4.0 і VS2012/4.5.
додано Автор Eren Ersönmez, джерело
@quetzalcoatl, я можу відтворити, що додавання try-catch навколо циклів уповільнює версію if/else у x86.
додано Автор Eren Ersönmez, джерело

Різниця насправді не має великого відношення до if/else vs ternary.

Дивлячись на джиттові розбирання (я не повернусь тут, PLS див. Відповідь @ 280Z28), виявляється, ви порівнюєте яблука та апельсини . У одному випадку ви створюєте два різних операції + = з постійними значеннями, і який ви вибираєте, залежить від умови, а в іншому випадку ви створюєте + = де значення додати залежить від стану.

Якщо ви хочете по-справжньому порівняти, якщо/інше чи трійкове, це було б більш справедливого порівняння (тепер обидва будуть однаково "повільнішими", або навіть можна сказати, що троєчніше трохи швидше):

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

проти

value += i > 0 ? 2 : 3;

Тепер розбирання для if/else стає таким, як показано нижче. Зауважте, що це трохи гірше, ніж трійковий випадок, оскільки він також припиняє використання регістрів для змінної циклу ( i ).

                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] 
40
додано
Я зробив тест, як ви запропонували: ввів іншу змінну diff , але трійка все ще набагато повільніше - зовсім не те, що ви сказали. Ви зробили експеримент перед публікацією цієї "відповіді"?
додано Автор user1032613, джерело
Ну, я б не казав, що це порівняння яблук та апельсинів. Обидва варіанти мають одну і ту саму семантику , тому оптимізатор може спробувати обидва варіанти оптимізації і вибрати, який з них є більш ефективним у випадку або .
додано Автор Vlad, джерело
Як щодо підкреслення порівняння яблук та апельсинів ?
додано Автор Ken Kin, джерело
@ Кен Кін впевнений, зробив.
додано Автор Eren Ersönmez, джерело

Edit:

Додано приклад, який можна зробити за допомогою оператора if-else, але не умовний оператор.


Перед відповіддю, будь ласка, перегляньте [ що швидше? ] на блозі пана Ліпперта. І я думаю, що Відповідь Ersönmez є найбільш правильною тут.

Я намагаюся згадати те, що ми повинні пам'ятати про мову програмування високого рівня.

По-перше, я ніколи не чув, що умовний оператор повинен бути швидшим або однаковою продуктивністю за допомогою оператора if-else в C♯ .

Причина проста в тому, що якщо операція if-else відсутня:

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

Вимогою умовного оператора є повинна бути значення з будь-якої сторони, а в C♯ він також вимагає, щоб обидві сторони : мали один і той же тип. Це просто робить його відмінним від оператора if-else. Таким чином, ваше запитання стає питанням про те, як створюється інструкція з машинного коду, щоб різниця в продуктивності.

З умовним оператором, семантично це:

Whatever the expression is evaluated, there's a value.

Але з твердженням if-else:

If the expression is evaluated to true, do something; if not, do another thing.

A value is not necessarily involved with if-else statement. Your assumption is only possible with optimization.

Інший приклад демонстрації різниці між ними буде таким:

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

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

код, наведений вище, компілює, однак, замінити оператор if-else на умовний оператор просто не буде компілювати:

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

Умовні оператори та statements if-else є концептуальними однаково, коли ви робите те ж саме, це можливо навіть швидше з умовним оператором в C , оскільки C є більш близьким до збірки платформи.


Для оригінального коду, який ви надали, умовний оператор використовується в foreach-loop, що може завалити ситуації, щоб побачити різницю між ними. Тому я пропоную наступний код:

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);
    }
}

і наступні дві версії IL оптимізовані, а не. Оскільки вони довгі, я використовую зображення для показу, а праворуч - це оптимізований:

(Клацніть, щоб побачити повнорозмірне зображення.)    hSN6s.png

У обох варіантах коду IL умовного оператора виглядає коротше, ніж оператор if-else, і все ще сумнівається в остаточному вигляді накопичений машинний код. Нижче наведені інструкції обох методів, а попереднє зображення не оптимізоване, останнє є оптимізованим:

  • Неоптимізовані інструкції: (натисніть, щоб переглянути повнорозмірне зображення.) ybhgM.png

  • Оптимізовані інструкції: (натисніть, щоб переглянути повнорозмірне зображення.) 6kgzJ.png

In the latter, the yellow block is the code only executed if i<=0, and the blue block is when i>0. In either version of instructions, the if-else statement is shorter.

Зауважте, що для різних інструкцій [ CPI ] не обов'язково однаковий. Логічно, для однакової інструкції більше інструкцій коштує більше циклу. Але якщо брати до уваги і час, і труб/кеш, що отримують інструкції, тоді реальний загальний час виконання залежить від процесора. Процесор також може передбачити гілки.

Modern processors have even more cores, things can be more complex with that. If you were an Intel processor user, you might want to have a look of [Intel® 64 and IA-32 Architectures Optimization Reference Manual].

Я не знаю, чи існує апаратно впроваджений CLR, але якщо так, то, швидше за все, ви отримаєте швидше з умовним оператором, оскільки IL, очевидно, менший.

Примітка: весь машинний код складається з x86.

8
додано

Я зробив те, що зробив Джон Скет, і пробіг через 1 ітерацію та 1000 ітерацій і отримав інший результат як від OP, так і від Джона. У моїй, трійковий трохи швидше. Нижче наведено точний код:

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();
    }

Вихід із моєї програми:

Час, що минув за запитом If-Else: 00: 00: 00.0140543

     

Тривалість часу для Ternary: 00: 00: 00.0136723

     

Час, що минув, якщо потрібно: 00: 00: 14.0167870

     

Час, що минув для Ternary: 00: 00: 13.9418520

Інший пробіг у мілісекундах:

Час, що минув, якщо потрібно: 20

     

Термін, що минув для Ternary: 19

     

Час, що минув для If-Else: 13854

     

Час, що минув для Ternary: 13610

Це працює в 64-bit XP, і я біг без налагодження.

Редагування - запуск у x86:

Існує велика різниця, використовуючи x86. Це було зроблено без налагодження на тому ж 64-розряднах комп'ютерах, як раніше, але для x86-процесорів. Це більше схоже на OP.

Час, що минув, якщо потрібно: 18

     

Тривалість часу для Ternary: 35

     

Час, що минув для If-Else: 20512

     

Час, що минув для Ternary: 32673

7
додано
@ user1032613 Я думаю, що може бути велика різниця, якщо ви працюєте без налагодження проти налагодження.
додано Автор CodeCamper, джерело
Не могли б ви спробувати на x86? Дякую.
додано Автор user1032613, джерело
@ user1032613 Я щойно відредагував мій пост із даними з x86. Це більше схоже на твій, де трійковий займає 2x повільніше.
додано Автор Shaz, джерело

Зібраний код асемблера розповість історію:

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

Генерує:

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

Оскільки:

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

Генерує:

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:

Таким чином, трійковий може бути коротшим і швидшим просто через те, що використовується менше інструкцій, і не стрибає якщо ви шукаєте істинне/невірне. Якщо ви використовуєте значення, відмінні від 1 і 0, то ви отримаєте той самий код, що і якщо/else, наприклад:

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

Генерує:

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:

Що таке ж, як/else.

5
додано

Виконати без налагодження ctrl + F5 здається, що відладчик сповільнює і тотальні, і трійковий, але, схоже, це сповільнює роботу троячного оператора набагато більше.

Коли я запускаю наступний код, це мої результати. Я думаю, що невелика різниця мілісекунд викликана компілятором, що оптимізує max = max і видаляє його, але, ймовірно, не робить оптимізації для трійкового оператора. Якщо хтось зможе перевірити збірку і підтвердити це, це буде чудово.

<�����������������������������������������������������������������������

���

������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������
4
додано

Дивлячись на створений IL, у цьому операції 16 операцій менше, ніж у операторі if/else (копіювання та вставлення коду @ JonSkeet). Однак це не означає, що це має бути швидшим процесом!

Щоб підсумувати відмінності в IL, метод if/else перекладається майже так само, як читає код C# (виконуючи доповнення у гілці), тоді як умовний код завантажує 2 або 3 на стек (залежно від значення) і потім додає його значення за межами умовного.

Інша різниця - це інструкція розгалуження. Метод if/else використовує brtrue (гілка, якщо true), щоб перейти за першим умовам, і безумовну гілку, щоб перейти від першого з оператора if. Умовний код використовує bgt (гілка, якщо більша), а не брюс, що, можливо, може бути більш повільним порівнянням.

Також (щойно прочитав про передбачення галузі) може бути штраф за виконання, якщо гілка буде меншою. Умовне відгалуження має лише одну команду в галузі, але якщо/else має 7. Це також пояснить, чому існує різниця між використанням довгого та int, тому що зміна до int зменшує кількість інструкцій у філіях if/else на 1 (змусити менше зачекати)

4
додано

У наступному коді, якщо/else, приблизно в 1.4 рази швидше, ніж трійковий оператор. Однак я виявив, що введення тимчасової змінної зменшує тривалість тривалості оператора приблизно в 1.4 рази:

Якщо/Else: 98 мс

     

Трійна: 141 мс

     

Триєрна з темпервир: 100 мс

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);
        }
    }
}
1
додано
var chat = new Chat();
var chat = new Chat();
642 учасників

Обсуждение вопросов по C# / .NET / .NET Core / .NET Standard / Azure Сообщества-организаторы: — @itkpi — @dncuug