Как стать автором
Обновить

Комментарии 46

Кодировал алгоритмы на C# и заметил что изменение позиции объявления переменной влияло на время исполнения на 20%, условно

int a=0;

int b=0;

менял на

int a=0, b=0;

Мне это очень не понравилось, по сути JIT пропускал кучи оптимизаций только из-за особенностей трассировки моего кода. Это эпикфейл MS.

Не знаю, как у вас там в С#, но для микроконтроллеров в классическом СИ в любой приличной IDE (IAR, KEIL, и т.д.) компиляторы имеют несколько режимов оптимизации уже давным-давно. И как раз такие вещи они прекрасно обрабатывают. Даже могут несколько строк кода выбрасывать из прошивки, если эти строки не имеют смысла. Во время отладки с непривычки разработчик может быть удивлён тем, что отладчик пропускает некоторые строки и даже блоки кода типа простых циклов со счётчиком, который нигде больше не используется (например, для задержки на 1 млн тактов).

Уверен, что для С# оптимизация настраивается от и до.

Уверен, что для С# оптимизация настраивается от и до.

Нет. Это по многим причинам не так. По большому счёту основная разница идёт между debug и release режимами - между ними разница значительна и, конечно, тестирование производительности в debug не стоит выполнять.
Компиляция C#->IL почти не настраивается и почти не оптимизирует (ну разве что на уровне вывода типов немного). Это, похоже, принципиальный подход.
JIT (IL в машинный код) тоже особо не настраивается - это скорее нужно для отладки самого движка .Net, чем для прикладных программистов. Причём JIT ограничен в оптимизациях - во-первых жёсткие требования к времени компиляции, во-вторых некоторые вещи нельзя оптимизировать "как в C" потому что нельзя по контракту.
В итоге .NET может выявить какие-то константные вычисления, некоторые peephole оптимизации, ограниченно может заинлайнить, может выкинуть неисполнимые ветки кода. Но, например, не делает оптимизацию tail recursion - скорее всего для сохранения возможности размотки стека при исключениях, возможно по этой же причине хвостовой call никогда не преобразует в jump. Расчёт констант и выражений, оптимизация циклов и глубина инлайнинга тоже очень далека от -O3 С/С++. Но не скажу, что всё совсем плохо (пользуясь случаем, скажу спасибо @Nagg за его вклад в дело оптимизации движка).

Важно отметить, что это не "C# плохой", это скорее "C# другой".

Но, например, не делает оптимизацию tail recursion - скорее всего для сохранения возможности размотки стека при исключениях, возможно по этой же причине хвостовой call никогда не преобразует в jump.

Вообще говоря, инструкция tail. в IL есть. Другое дело, что компилятор C# очень тяжело заставить её применять. А вот F# и Nemerle активно используют хвостовую рекурсию, если это возможно (например, нет обработки исключений с вызовом рекурсивной функции внутри).

Ну что же, век живи - век учись. Спасибо за наводку. Я не знал о её существовании. В любом случае, C/C++ компилятору проверить допустимость хвостовой рекурсии обычно проще (и поэтому она происходит).

Иногда под дотнетом очень интересные оптимизации встречаются. Например, тот же F# часто вообще рекурсию в цикл преобразует.

Пример с факториалом

Было:

let fact n =
    let rec fact_r n acc =
        match n with
        | 1 -> acc
        | n -> fact_r (n - 1) (acc * n)
    if n > 1 then fact_r n 1 else 1

Стало:

_.fact_r@2(Int32, Int32)
    L0000: push ebp
    L0001: mov  ebp, esp
    L0003: lea  eax, [ecx-1]
    L0006: test eax, eax
    L0008: ja   short L000e
    L000a: mov  eax, edx
    L000c: pop  ebp
    L000d: ret
    L000e: imul edx, ecx
    L0011: mov  ecx, eax
    L0013: jmp  short L0003
_.fact(Int32)
    L0000: cmp  ecx, 1
    L0003: jle  short L0010
    L0005: mov  edx, 1
    L000a: call _.fact_r@2(Int32, Int32)
    L000f: ret
    L0010: mov  eax, 1
    L0015: ret

Эквивалентный код на C#:

internal static int fact_r@2(int n, int acc)
{
    while (true)
    {
        if (n == 1) return acc;
        acc *= n;
        n = n - 1;
    }
}
public static int fact(int n)
{
    if (n > 1) return fact_r@2(n, 1);
    return 1;
}

Это и есть пример оптимизации хвостовой рекурсии:

Перезаписать аргументы и перейти к началу функции.

НЛО прилетело и опубликовало эту надпись здесь

Только не инструкция, а префикс инструкции.

Вот только в своё время изменение обработки этого префикса в JIT-компиляторе вызвало некоторое удивление у некоторых людей.

Не скажу за детали, но суть была в том, что некоторый человек ориентировался на то, что tailcall игнорируется и некоторый ресурсоёмкий сервис в облаке падает со stack overflow, а после обновления рантайма он внезапно стал использовать хвостовую рекурсию и не падал, а пользователю за работу сервиса был выставлен немалый счёт (это в телеге в pro.net как-то обсуждалось)

Я, конечно, по профессии нифига не программист и код пишу исключительно для автоматизации работы или в качестве хобби, но мне кажется, строить логику работы программы на ошибках — это неимоверная дичь.

Хотелось бы увидеть какой-то тест, не один год в C#, никогда не замечал подбного.

Так как JIT о C# ничего не знает, это надо на iL смотреть.

НЛО прилетело и опубликовало эту надпись здесь

Я думаю было бы хорошо иметь полный пример

Нет особой разницы в тем что я написал выше, так было:

static void strfind3(byte[] s, byte[] su)
{
  int ju = 0;
  int c = 0;
  int m = 0;
  int sl = s.Length;
  int sul = su.Length;
  int eof = sl - sul;
  int sp = 0;
  // всякое...
}

этот код при обработке файла в 900М отрабатывал за 2400 мс, а этот код:

static void strfind3(byte[] s, byte[] su)
{
  int ju = 0, c = 0, m = 0, sp = 0, j, sl = s.Length, sul = su.Length, eof = s.Length - sul;
  // всякое
}

уже за 1800 мс. Была однозначная повторяемость результатов.

Позднее в другой программе тоже игрался с объявлением переменных, там код

int i = 0, compares = 0, matches = 0, sul = substr.Length, sl = str.Length;

менялся на

int compares = 0, matches = 0, sul = substr.Length, sl = str.Length;
int i = 0;

и выполнение ВСЕЙ программы замедлялось на 40%.

Я пишу без ООП, короткие программы, я не разработчик, проверяю какие-то алгоритмы и т.п. и для меня такое поведение - нонсенс, так как еще с Turbo C я прекрасно помню как сам компилятор оптимизирует код.

и желательно на https://sharplab.io/

я не занимаюсь публикацией своих программ.

Не обязательно же всю программу.
Я сделал пример, но в нём разница на 1 mov (и то, зависит просто от кода ниже)

Надеюсь, что разница именно в release режиме?

Наличие этой разницы уже многое говорит

Ни о чём не говорит. Это даже на синтетическом примере не даст и 3% разницы.

НЛО прилетело и опубликовало эту надпись здесь

Сейчас мы НЕ доказываем что код меняется на 40% производительности, мы показываем суть моего треда - при синонимичном изменении кода меняется состав инструкций ЦПУ в конечном коде. Важно именно это, а не сколько там скушает mov.

Сейчас мы НЕ доказываем что код меняется на 40% производительности

В начале топика вы четко сказали про 20%:


Кодировал алгоритмы на C# и заметил что изменение позиции объявления переменной влияло на время исполнения на 20%, условно

3-20-40 абсолютно не важно, так как проблема не в процентах.

если не всю, то я уже вам дал всё необходимое. а без всей программы эффект может и не наблюдаться. вон ниже уже начали петь песню про неправильное тестирование, но это не то совсем.

У автора там "опечатка" или хз как ее назвать. Если ее убрать - скомпилированный код будет идентичный. SharpLab

Да, упустил и не проверил внимательно - как раз к тому, что от небольшой семантически значимой разницы может измениться код. Это отличие было в и коде комментария. Там еще и j определена была.

Я пишу без ООП, короткие программы, я не разработчик, проверяю какие-то алгоритмы и т.п. и для меня такое поведение — нонсенс,

Возможно, вы просто не умеете замерять производительность.

Ох, любимый комментарий XD Если его не пишут, то считайте день прошёл зря.

Вы подразумеваете, что если программа А в одних и тех же условиях выполняется то 1 минуту, то - 5, то виноват не код программы (язык, компилятор, оптимизации), а процедура замера? (надеюсь вам же не приходит в голову, что два подряд объявления int не являются синонимичными записи с одним int???)

Это как на олимпийских играх для бегунов на 400 метров поставить две пары фотоэлементов и регистрировать старт и финиш, а потом кто-то скажет, что есть квантовая суперпозиция и ваш способ замера не учитывает искривление пространства-времени.

Да, измерять производительность не так просто. Это не про "искривление пространства-времени". Судя по разнице и кейсу, скорее всего у вас разница проявляется на debug режиме. Могу ошибаться, но вы же так и не предоставили конкретные кейсы и замеры. И в этом случае у вас вашим бегунам надо одному после каждых 10 метров отмечаться в бланке о пробегании 10 метров, а второму - раз в 50 метров. В такой процедуре уже неважно, чем вы измеряли - фотофинишем или ручным секундомером, потому что скорость бега на 400 метров вы не измеряете.
База с которой в принципе можно начинать замерять производительность - поставить максимально в одинаковые "стандартные" условия измеряемые кейсы (в .NET стал стандартным инструмент BenchmarkDotNet, в JVM - JMH). Следующая часть - зафиксировать и описать окружение. Следующая - сформулировать гипотезу (в данном случае - что однострочное объявление лучше многострочного). Потом - проверить гипотезу. Потом, если она подтверждается замерами - максимально изолировать причину и уменьшить влияние других факторов. По результатам уже можно было бы поднять issue в проекте dotnet runtime.
Да, бывают какие-то отклонения от этой процедуры, например, если бага в инлайнинге jit, то она как раз НЕ проявляется в debug, а проявляется в release (я такой кейс находил). Но это всё равно примерно тот же сценарий (только код был одинаковый, а окружения отличались ровно на опцию debug/release).

Это не дебаг, что очевидно.

Мне не нужен специальный инструмент для измерений если я имею статистику и повторяемость результата при изменении кода. Но если вам так легче:

// * Summary *

BenchmarkDotNet=v0.13.1, OS=Windows 10.0.19044.1706 (21H2)
  [Host]     : .NET 6.0.5 (6.0.522.21309), X64 RyuJIT
  DefaultJob : .NET 6.0.5 (6.0.522.21309), X64 RyuJIT

|        Method |    Mean |    Error |   StdDev |
|-------------- |--------:|---------:|---------:|
|             1 | 1.237 s | 0.0054 s | 0.0048 s |
|             2 | 2.282 s | 0.0085 s | 0.0079 s |

Что это изменило? XD

Так полный код предоставьте, на котором такое поведение наблюдается, иначе это все равно голословно.

1) NDA

2) Мы не в суде, я сообщил информацию к сведению, вы можете её проигнорировать

2) Мы не в суде, я сообщил информацию к сведению, вы можете её проигнорировать

Ну я, в свою очередь, постарался донести свое мнение до других пользователей, которые прочитают ваше сообщение.

Это поведение в стиле капитана очевидности, так как вся информация не подкрепленная фактами является голословной, для этого не требуется от вас никаких сообщений. Вы же попросили еще и код, хотя я неоднократно уже заявил что его не предоставлю.

Ну для новчиков может быть не очевидно — прочитают первое сообщение и примут его за чистую монету.

школьники начальных классов?

Старших классов тоже.

1 лучше почисти метрики от контекста, часто nda содержит размытое определение выдачи конфиденциальной информации

ась?

У вас не идентичный код, что выше привели вы. У вас не только изменение в объявлении, но и само присваивание для eof совершенно подругому описано. Ваша проблема может состоять в том, что для вычисления eof переменная S уже ушла из кеша процессора например

Вот смотрите, у вас ОС Windows, вы её написали. Кода на 1 млн страниц, а производительной ВСЕЙ системы определяется тем как именно в самой первой строке кода произошло присваивание eof, не скорость запуска системы, не скорость выполнения какого-то участка кода, а скорость работы вообще всей ОС. Вам не кажется, что ваше открытие тут ничего не значит?

Неужели все это стоит учитывать?

Голословное утверждение, ничем не подкрепленное.

у меня нет задачи сделать вас своим адептом, но !!!! все, кто пользуется C# не на уровне "Hello World!" знают, что ассемблерный код может очень сильно меняться при несущественных изменениях в программе, просто я лично не ожидал, что эти изменения могут быть практически фатальными для производительности. Я привык, что компилятор умнее меня с точки зрения оптимизации, я это видел и вижу при работе с MOS 6502, с 8088/80386, во всяком случае я не жду от них такой подставы.

Однако оба эти подхода привели бы к критическому изменению, поскольку этот метод является частью публичного API.

Разве сейчас контракт функции не поменялся (пусть и не так заметно)? До этого возвращался reference на копию, а сейчас на оригинал. Гипотетически можно представить клиента, который менял полученное правило и при этом никак не влиял на других клиентов. Теперь это изменится

А нельзя просто написать r[i].Match(...)? Зачем это переменная вообще нужна?

Конечно, если как выше написано в комментарии, чтобы отдать копию, то тогда автор статьи рукожоп (не переводчик, переводчик молодец).

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории