Защо добавянето на локални променливи прави .NET кода по-бавен

Защо коментирането на първите два реда от този for цикъл и разкоментирането на третия води до 42% ускоряване?

int count = 0;
for (uint i = 0; i < 1000000000; ++i) {
    var isMultipleOf16 = i % 16 == 0;
    count += isMultipleOf16 ? 1 : 0;
    //count += i % 16 == 0 ? 1 : 0;
}

Зад синхронизирането стои значително различен асемблерен код: 13 срещу 7 инструкции в цикъла. Платформата е Windows 7 с .NET 4.0 x64. Оптимизацията на кода е активирана и тестовото приложение е стартирано извън VS2010. [Актуализация: Repro проект, полезен за проверка на настройките на проекта.]

Елиминирането на междинното булево значение е фундаментална оптимизация, една от най-простите в моята ера от 1980 г. Dragon Книга. Как оптимизацията не беше приложена при генериране на CIL или JIT на x64 машинния код?

Има ли превключвател „Наистина компилатор, бих искал да оптимизирате този код, моля“? Въпреки че симпатизирам на мнението, че преждевременната оптимизация е подобна на любовта към парите, можех да видя разочарованието в опитите да профилирам сложен алгоритъм, който имаше проблеми като този, разпръснати из неговите процедури. Ще работите през горещите точки, но нямате намек за по-широкия топъл регион, който може да бъде значително подобрен чрез ръчно настройване на това, което обикновено приемаме за даденост от компилатора. Силно се надявам да пропускам нещо тук.

Актуализация: Разлики в скоростта също възникват за x86, но зависят от реда, в който методите се компилират точно навреме. Вижте Защо редът на JIT влияе върху производителността?

Код за сглобяване (според заявката):

    var isMultipleOf16 = i % 16 == 0;
00000037  mov         eax,edx 
00000039  and         eax,0Fh 
0000003c  xor         ecx,ecx 
0000003e  test        eax,eax 
00000040  sete        cl 
    count += isMultipleOf16 ? 1 : 0;
00000043  movzx       eax,cl 
00000046  test        eax,eax 
00000048  jne         0000000000000050 
0000004a  xor         eax,eax 
0000004c  jmp         0000000000000055 
0000004e  xchg        ax,ax 
00000050  mov         eax,1 
00000055  lea         r8d,[rbx+rax] 
    count += i % 16 == 0 ? 1 : 0;
00000037  mov         eax,ecx 
00000039  and         eax,0Fh 
0000003c  je          0000000000000042 
0000003e  xor         eax,eax 
00000040  jmp         0000000000000047 
00000042  mov         eax,1 
00000047  lea         edx,[rbx+rax] 

person Edward Brey    schedule 29.04.2012    source източник
comment
Ще ми е любопитно да видя различния асемблерен код. Бихте ли го публикували?   -  person phoog    schedule 29.04.2012
comment
тествахте ли bool isMultipleOf16 = ...?   -  person David.Chu.ca    schedule 29.04.2012
comment
@David.Chu.ca - това няма да има значение - var е компилатор, моля, направете извод за типа на тази променлива и се преструвайте, че вместо това съм написал това. В този случай той ще е извел bool за себе си.   -  person Damien_The_Unbeliever    schedule 29.04.2012
comment
какво ще кажете за статичен isMulitppleOF16?   -  person David.Chu.ca    schedule 29.04.2012
comment
@EdwardBrey: Тъй като направихте това в режим Debug, всички залози са изключени   -  person BrokenGlass    schedule 29.04.2012
comment
Единственият начин да получите този код е като деактивирате оптимизатора на трептене. Невалиден тест.   -  person Hans Passant    schedule 29.04.2012
comment
@BrokenGlass Звучиш доста уверено. Бях внимателен, за да се уверя, че съм в режим на освобождаване и че оптимизациите остават активирани. За да проверя, изключих оптимизацията и проверих, че времената стават по-бавни и асемблерният код става по-голям. Промених конфигурацията на проекта на Debug и включих настройката Suppress JIT optimization on module. Когато се върнах към оптимизирано издание без потискане, кодът отново стана по-малък и по-бърз. Ако правя грешка, има ли нещо, на което трябва да обърна внимание, за да го забележа?   -  person Edward Brey    schedule 29.04.2012
comment
@Hans Опитах два отделни компютъра със същия резултат. Също така внедрих повторно в множество проекти, защото не можех да повярвам, че е наистина вярно. Все пак същият резултат. Не съм направил нищо, което знам, за да деактивирам JIT оптимизаторите на моите системи, въпреки че не знам къде да търся това. Можете ли да възпроизведете?   -  person Edward Brey    schedule 29.04.2012
comment
Разбира се, мога да получа същия код, който публикувахте, като включа опцията за отстраняване на грешки за оптимизиране на JIT. Оптимизираният код е много различен.   -  person Hans Passant    schedule 29.04.2012
comment
@Hans 13-те реда асемблерен код по-горе са с изключена оптимизация на Suppress JIT. Ако включа потискането, става още по-лошо и се издига до 21 реда асемблен код.   -  person Edward Brey    schedule 29.04.2012
comment
Не мога да възпроизвеждам резултата ви с точно същата версия на OS и VS+.NET, нямам представа защо получавате неоптимизиран код.   -  person Hans Passant    schedule 29.04.2012
comment
@Hans Нека се опитаме да стесним това. Поставих repro в GitHub. Какво получавате за резултатите?   -  person Edward Brey    schedule 29.04.2012
comment
@EdwardBrey: Не мога да намеря източник в момента, но вярвам, че трептенето и/или други настройки на оптимизатора са различни, ако имате прикачен дебъгер въобще (тоест, ако сте работещ от Visual Studio, дори ако сте компилирали в режим Release). Опитайте да стартирате кода си от командния ред (не от VS) и вижте какво ще се случи.   -  person Daniel Pryden    schedule 30.04.2012
comment
@Daniel Проведох всичките си тестове за време от командния ред извън Visual Studio. Работих само в рамките на Visual Studio, за да получа списъци с асемблиращи кодове. Като двойна проверка, току-що се опитах да стартирам теста за синхронизиране с Visual Studio, който дори не работи. Все още виждам различните времена.   -  person Edward Brey    schedule 30.04.2012
comment
@EdwardBrey: Изпълнението на кода като x86 кара и двата да работят по едно и също време. Редактиране: Но само на .NET 4. .NET 2 има разлика и с двете.   -  person leppie    schedule 30.04.2012
comment
@leppie Бихте ли изпробвали конфигурацията x86 в тестов проект в github? (Github има бутон ZIP за бързо хващане на zip файл на проекта.) Получавам много различни времена между тестовете с един и два израза, дори за x86.   -  person Edward Brey    schedule 30.04.2012
comment
@EdwardBrey: Това направих.   -  person leppie    schedule 30.04.2012
comment

Имам формуляр, върху който работя, и моят JavaScript за валидиране (jQuery & jQuery validate plugin) работи до определен момент. Настроих jsfiddle, за да го покажа в работно състояние: http://jsfiddle.net/5Ukv2/

Въпреки това, когато използвам функцията/параметъра errorPlacement, тя се поврежда. Грешките отиват там, където съм посочил, но множество грешки се добавят всеки път, когато актуализирам въвеждането (т.е. всяко натискане на клавиш), и когато записът е валиден според JavaScript, грешките не се премахват от моя HTML: http://jsfiddle.net/5Ukv2/1/

Освен това проверката започва да се извършва веднага щом напиша в полето за въвеждане; този проблем е незначителен в сравнение с допълнителните и неотстранените елементи на грешка.

какво правя грешно

Кодът е приложен по-долу за справка, както и в jsfiddles. Коментирах няколко метода, защото те правят ajax извиквания, които няма да работят тук, но валидирането действа по същия начин.

HTML:

<form action="email_submit.php" method="post" name="enterForm" id="enterForm">
    <label for="email">Enter your email to play: 
    <input type="text" name="email" id="email" value=""  /> 
    <input type="submit" value="Enter" name="submit" class="submit" />       
</form>

JavaScript:

// JavaScript Document

$(function(){

$("#enterForm").validate({
    rules: {
        email: {
            email: true,
            required: true,
            remote: {url:"email_valid.php", async:false}
        }
    },
    messages:{
            email:{
                email: "Please enter a valid email address",
                required: "Please enter an email address",
                remote: "That is not a valid domain"
                }
    },
    submitHandler: function(form) {

        var formData = $('form').serialize();
        $.ajax({
          type: 'POST',
          url: "email_submit.php",
          data: formData,
          success: function(data){
              if(data.subscriber && data.valid_email)
                window.location = "calendar.html";
              else if( !data.valid_email)
                alert("Please enter a valid email");
              else if( !data.subscriber)
                alert("Open sign up form");
          },
          error: function(data){
              alert("e:" + data);
              console.log(data);
          },
          dataType: "JSON"
        });

        return false;
    },
    errorPlacement: function (error, element){
        if(  element.attr("name") == "email" ){
            error.insertAfter("#enterForm");
        } 
        else
            error.insertAfter(element);
    }               

    });

});
  -  person Edward Brey    schedule 30.04.2012
comment
Когато изхвърлям компилирания JIT код за двете версии (чрез WinDbg), виждам разлики, но също така виждам значително повече код, отколкото изброявате във вашия въпрос. Не забравяйте, че няма праволинейно съпоставяне между изходните линии и JIT компилирания код, така че трябва да го изброите целия. Сравняването само на тялото на цикъла може да не предостави достатъчно подробности за сравнение.   -  person Brian Rasmussen    schedule 02.05.2012


Отговори (5)


Въпросът трябва да бъде "Защо виждам такава разлика на моята машина?". Не мога да възпроизведа такава огромна разлика в скоростта и подозирам, че има нещо специфично за вашата среда. Въпреки това е много трудно да се каже какво може да бъде. Може да има някои (компилаторни) опции, които сте задали преди време и сте забравили за тях.

Създадох конзолно приложение, възстанових в режим на издаване (x86) и стартирах извън VS. Резултатите са почти идентични, 1,77 секунди и за двата метода. Ето точния код:

static void Main(string[] args)
{
    Stopwatch sw = new Stopwatch();
    sw.Start();
    int count = 0;

    for (uint i = 0; i < 1000000000; ++i)
    {
        // 1st method
        var isMultipleOf16 = i % 16 == 0;
        count += isMultipleOf16 ? 1 : 0;

        // 2nd method
        //count += i % 16 == 0 ? 1 : 0;
    }

    sw.Stop();
    Console.WriteLine(string.Format("Ellapsed {0}, count {1}", sw.Elapsed, count));
    Console.ReadKey();
}

Моля, всеки, който има 5 минути, копира кода, възстанови, стартира извън VS и публикува резултатите в коментари към този отговор. Бих искал да избегна да казвам "работи на моята машина".

РЕДАКТИРАНЕ

За да съм сигурен, създадох 64-битово приложение Winforms и резултатите са подобни на тези във въпроса - първият метод е по-бавен (1,57 сек) от втория ( 1,05 сек). Разликата, която наблюдавам е 33% - пак много. Изглежда, че има грешка в .NET4 64-битов JIT компилатор.

person Maciej    schedule 30.04.2012
comment
Първи метод: 1.8736291s, втори метод: 1.8566318s на моята машина, възстановен с Release (x86), стартира извън VS, използвайки абсолютно същия код. - person ShdNx; 30.04.2012
comment
Трябва да направите нещо с count (като например да го включите в изявлението си WriteLine). В противен случай оптимизаторът извършва селективна оптимизация, която се променя с времето. - person Edward Brey; 30.04.2012
comment
@EdwardBrey, промених 64-битовите си тестове и получих същите резултати като вас сега. Коригирах отговора си, за да отразя. - person Maciej; 30.04.2012
comment
@EdwardBrey, мога да го възпроизведа само в 64-битово приложение - person Maciej; 30.04.2012
comment
Получих 1,86s (1-ви) и 1,38s (2-ри) в x64, докато в x86 получих около 2,09s и за двата метода. - person Ral Zarek; 30.04.2012
comment
Актуализирах тестовия проект в github, за да включва x64 и x86 конфигурации. След извършване на пакетно повторно изграждане на двете конфигурации и стартиране на всеки .exe извън VS2010 на два компютъра (и двата Core i5), виждам много различни времена за Test1 и Test2 както на x64, така и на x86. Maciej и Marcus, за да помогнете за елиминирането на променливите, бихте ли могли всеки да вземе проекта и да го опита? (Github има ZIP бутон за бързо грабване на zip файл на проекта.) - person Edward Brey; 30.04.2012
comment
@EdwardBrey Изпълнението на вашия тест ми дава по-бавно изпълнение на версия Multiline и на двете платформи. Но ако променя теста, така че да изпълнява 4xMultiline и след това 4xSingleline, няма разлика в скоростта на x86 (x64 не се влияе) - person Maciej; 01.05.2012
comment
@Maciej +1 за страхотно наблюдение. Изглежда, че кой метод се извиква първи е това, което има значение. Това изглежда почти сигурно се дължи на реда на JIT. Но защо редът на JIT има значение е объркващо. Актуализирах тялото на въпроса с връзка към новия въпрос, който вашето наблюдение подтикна . - person Edward Brey; 02.05.2012
comment
@EdwardBrey Можете ли да гласувате с моя отговор вместо коментара, тъй като това ще покаже истинска признателност :-) - person Maciej; 02.05.2012
comment
@Maciej Съжалявам, но не мисля, че вашият отговор наистина отговаря на въпроса. Това ни помогна да открием объркващ проблем, а именно, че за x86 санкциите за подравняване могат да накарат добре оптимизирания код да работи толкова бавно, колкото не напълно оптимизирания код. Но все още остава въпросът, защо има случаи, в които на първо място се генерира не толкова напълно оптимизиран x64 (и x86) код? - person Edward Brey; 02.05.2012

Не мога да говоря с .NET компилатора или неговите оптимизации, или дори КОГА извършва своите оптимизации.

Но в този конкретен случай, ако компилаторът сгъне тази булева променлива в действителния оператор и вие трябваше да опитате да отстраните грешки в този код, оптимизираният код няма да съвпада с кода, както е написан. Няма да можете да преминете през присвояването isMulitpleOf16 и да проверите неговата стойност.

Това е само един пример за това къде оптимизацията може да бъде изключена. Може да има и други. Оптимизацията може да се случи по време на фазата на зареждане на кода, а не във фазата на генериране на код от CLR.

Модерните времена за изпълнение са доста сложни, особено ако добавите JIT и динамична оптимизация по време на изпълнение. Чувствам се благодарен, че понякога кодът изобщо прави това, което казва.

person Will Hartung    schedule 29.04.2012
comment
Чудех се на себе си, когато видях асемблерния код дали по някакъв начин оптимизацията е деактивирана. Получих асемблерния код, като спрях на точка на прекъсване в дебъгера VS2010 и използвах прозореца за разглобяване (докато времето получих, като стартирах без дебъгер). Като тест включих Инструменти › Опции › Дебъгване › Общи › Потискане на оптимизацията на JIT при настройката на модула. Разбира се, асемблиращият код стана още по-голям. - person Edward Brey; 29.04.2012
comment
В родния свят на C++ е напълно нормално точките на прекъсване и редът на кода да са странни, когато оптимизациите са активирани. По същия начин променливи като isMultipleOf16 не винаги са налични в програмата за отстраняване на грешки. Ето защо има режим за отстраняване на грешки. В крайна сметка все още изпълняваме същия машинен код, така че не виждам защо CLR ще прави нещата по-различни. Наистина, когато възникнат изключения в C#, дори в режим на отстраняване на грешки, понякога получавам съобщение за оптимизиране на стойността на променлива, дори в режим на отстраняване на грешки. - person Edward Brey; 29.04.2012
comment
+1 за забележката, че настройките за отстраняване на грешки могат да повлияят на генерирането на код. - person Marco van de Voort; 29.04.2012

Това е грешка в .NET Framework.

Е, наистина само спекулирам, но изпратих доклад за грешка на Microsoft Connect, за да видите какво казват. След като Microsoft изтри този отчет, аз го изпратих отново в проекта roslyn в GitHub.

Актуализация: Microsoft премести проблема в проекта coreclr. От коментарите по въпроса, наричането му бъг изглежда малко силно; това е по-скоро липсваща оптимизация.

person Edward Brey    schedule 30.04.2012
comment
Ако имах един долар за всеки път, когато програмист ми каже, Моят код не работи. Трябва да е грешка в рамката (или компилатор или библиотека за изпълнение и т.н.), а по-късно разбрах, че това е грешка в собствения му код, мога да се пенсионирам. - person Jim Mischel; 02.05.2012
comment
@Jim: Самият аз съм го виждал много пъти. Най-добрият противоотрова, който познавам, е да изолира поведението възможно най-фундаментално и да предостави на продавача репродукция. И да запазите изчаквателна позиция. Ето къде сме. - person Edward Brey; 07.05.2012
comment
@TankorSmash Може би Microsoft го е изтрил като част от преместването на кода в GitHub. Вече не е на таблото ми за управление на Connect. Изглежда, че много проблеми, за които докладвах, са изчезнали. Някакъв вид известие би било добре. Изпратих отново проблема в проекта GitHub и съответно актуализирах отговора. - person Edward Brey; 04.11.2015

Мисля, че това е свързано с другия ви въпрос. Когато променя кода ви по следния начин, многоредовата версия печели.

опа, само на x86. При x64 многоредовото е най-бавното и условното ги побеждава с лекота и двете.

class Program
{
    static void Main()
    {
        ConditionalTest();
        SingleLineTest();
        MultiLineTest();
        ConditionalTest();
        SingleLineTest();
        MultiLineTest();
        ConditionalTest();
        SingleLineTest();
        MultiLineTest();
    }

    public static void ConditionalTest()
    {
        Stopwatch stopwatch = new Stopwatch();
        stopwatch.Start();
        int count = 0;
        for (uint i = 0; i < 1000000000; ++i) {
            if (i % 16 == 0) ++count;
        }
        stopwatch.Stop();
        Console.WriteLine("Conditional test --> Count: {0}, Time: {1}", count, stopwatch.ElapsedMilliseconds);
    }

    public static void SingleLineTest()
    {
        Stopwatch stopwatch = new Stopwatch();
        stopwatch.Start();
        int count = 0;
        for (uint i = 0; i < 1000000000; ++i) {
            count += i % 16 == 0 ? 1 : 0;
        }
        stopwatch.Stop();
        Console.WriteLine("Single-line test --> Count: {0}, Time: {1}", count, stopwatch.ElapsedMilliseconds);
    }

    public static void MultiLineTest()
    {
        Stopwatch stopwatch = new Stopwatch();
        stopwatch.Start();
        int count = 0;
        for (uint i = 0; i < 1000000000; ++i) {
            var isMultipleOf16 = i % 16 == 0;
            count += isMultipleOf16 ? 1 : 0;
        }
        stopwatch.Stop();
        Console.WriteLine("Multi-line test  --> Count: {0}, Time: {1}", count, stopwatch.ElapsedMilliseconds);
    }
}
person Ben Voigt    schedule 02.05.2012
comment
Актуализирах repro проекта, за да включва if тест. Правил съм едноредови и многоредови варианти. Както на x64, така и на x86, едноредовата версия е по-бърза (когато няма наказание за подравняване). Също така направих варианти, които изобщо нямат условен код в цикъла (само битова математика). На х86 са врат и врат (сглобката не съм я проверявал). На x64 версията с локална променлива работи по-бързо! Все още е удивително, че локалната променлива изобщо има значение. - person Edward Brey; 03.05.2012

Склонен съм да мисля за това по следния начин: хората, които работят върху компилатора, могат да правят толкова много неща на година. Ако през това време можеха да внедрят ламбда или много класически оптимизации, бих гласувал за ламбда. C# е език, който е ефективен по отношение на усилията за четене и писане на код, а не по отношение на времето за изпълнение.

Така че е разумно екипът да се концентрира върху функции, които максимизират ефективността на четене/запис, а не ефективността на изпълнение в определен ъглов случай (от които вероятно има хиляди).

Вярвам, че първоначално идеята беше JITter да извърши цялата оптимизация. За съжаление JITting отнема значително време и всякакви разширени оптимизации ще го влошат. Така че това не се получи толкова добре, колкото се надяваше.

Едно нещо, което открих за програмирането на наистина бърз код в C#, е, че доста често срещате сериозно затруднение на GC, преди всяка оптимизация, като споменатата от вас, да има значение. Както ако разпределите милиони обекти. C# ви оставя много малко по отношение на избягване на разходите: вместо това можете да използвате масиви от структури, но полученият код е наистина грозен в сравнение. Искам да кажа, че много други решения относно C# и .NET правят такива специфични оптимизации по-малко полезни, отколкото биха били в нещо като C++ компилатор. По дяволите, те дори премахнаха специфичните за CPU оптимизации в NGEN, производителност на търгуване за ефективност на програмист (дебъгер).

Като казах всичко това, бих харесвал C#, който действително използва оптимизации, които C++ използва от 90-те години на миналия век. Просто не за сметка на функции като, да речем, async/await.

person Roman Starkov    schedule 06.05.2012
comment
Бих бил изключително предпазлив да чета твърде много в статии от 2005 г. и .net 1.1! Доста много неща се промениха през последните 7 години. - person Jason Williams; 06.05.2012