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

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

Таким образом, проверка события на null и вызов его обработчика будут выполнены в виде одной команды, что обеспечит безопасный вызов события.


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

Там все еще хуже. "Стандарт" C# не запрещает оптимизатору использовать вместо поля локальную переменную или наоборот когда он уверен, что там одинаковые значения — если только поле не объявлено как volatile. Слово "стандарт" я взял в кавычки — потому что полного стандарта у C# нет, особенно в части модели памяти, вместо этого есть куча постов в блогах у авторов языка.


Поэтому, формально даже код с явным "вытягиванием" в локальную переменную может работать некорректно!


var unload = this.unload; // Оптимизатор имеет право "выкинуть" эту переменную и использовать поле
if (unload != null) unload();

Но, "по счастливой случайности", все известные реализации оптимизируют доступ к полям через локальные переменные, а не наоборот (иначе бы дырку быстро заткнули). По той же причине явно неправильный код может нормально работать:


if (this.unload != null) this.unload(); // Есть шанс, что в результате оптимизации ошибка пропадет

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


this.unload?.Invoke(); // Некуда оптимизировать
this.unload?.Invoke();

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

Ну, можно компилировать компилятором, идущим в составе Microsoft Build Tools 2015 — главное же получить бинарник, дальше версия языка не имеет значения, как и версия рантайма. Ну или самому компилятор собрать — исходный код Roslyn, кажется, открыт.


И поскольку сам компилятор написан на C# — его можно попытаться запустить через тот же Mono...


Но это я так, в порядке шутки.

Мсье понимает (С) ))
лавное же получить бинарник, дальше версия языка не имеет значения, как и версия рантайма.

Абсолютно неверно. Советую попробовать скомпилировать coroutine в рантайме юнити и в рантайме C# .net и посмотреть рефлектором / испаем — абсолютно разный код. По той же причине неправильно будут работать BeginInvoke и тп штуки если их скомпилить свежим рантаймом в dll и положить в юнити. Проблема — в патченном моно из юнити.

Но это же не означает, что они несовместимы!

Они чисто условно совместимы. Попробуйте скомпилировать нечто специфическое, async / await, Task-и или что-нибудь этакое из C# — по такой логике «оно должно работать, это же C#!» :) Всегда нужно учитывать рантайм, на котором будет работать байткод. + сейчас юнитеки везде пропихивают il2cpp (с 5.4 стал официальным рантаймом для android), что тоже может повлечь за собой неадекватное поведение кода, скомпилированного не в родном для юнити окружении.
Есть такая библиотечка — https://github.com/sta/websocket-sharp, реализующая вебсокеты поверх штатных сокетов. Ну и до юнити5 сокеты были отрублены. Ну как отрублены — их криворукие индусы в билд-процедуре проверяли типы в клиентской сборке — не начинаются ли они с System.Net.Sockets и прерывать процесс с эксепшном «насяльника, купите прошку, кушать-ма ощинь хоцца». И тут же проверяли whitelist сборок, в которых разрешены были эти типы. Теперь уже можно рассказать очень простой способ обхода этого запрета — просто берешь и делаешь неймспейс, начинающийся с Systemxxx + даешь имя сборки с таким же именем (у меня была SystematicLaziness.dll :) ) и тест проходит, те в такой сборке можно было гонять сокеты без проблем. Вот берешь эту MIT-овскую библиотечку, меняешь неймспейс, компилишь — все вроде работает в редакторе, а вот на реальных девайсах начинаются чудеса (особенно после AOT тогда еще на ios). Собственно, автор подхачил ее на совместимость с моно (проблема была в вызове асинхронных методов) и выложил на ассетстор — в реп фиксы не пошли, но можно было погуглить и найти решение.

Я компилил async / await для .NET 2.0 — сомневаюсь, что под измененный Mono это будет сложнее.

А можно привести код, полученный рефлектором / илспаем после такой генерации в dll? Потому что это малореально в рантайме 2.0 (если только через ThreadPool и кучу сахара).

Он ничем не отличается от кода под 4.5


Надо просто подсунуть нужные классы, не важно в какой сборке. Я их в то время писал сам — но сейчас советую просто вытащить нужные файлы из исходников Mono или CoreCLR

Еще раз — в юнити mono 2.6.3 до сих пор, ни о каких .net4.5 и даже .net4 разговора не идет. Соберите в честный .net2.0 — оно не заработает.

Заработает, если добавить в проект требуемые классы. Точно так же, как у меня все заработало в честном 2.0


Да, у меня в проекте был класс Task в пространстве имен System.Threading.Tasks. Ну и что?

Кто из нас слепой? https://github.com/mono/referencesource/tree/mono-4.4.0-branch/mscorlib/system/threading/Tasks


Разумеется, я не имел в виду вытащить класс из той же самой версии Mono, которая его не имеет! Его надо вытащить в виде исходного кода из той версии, в которой этот класс есть, и положить среди своих файлов.

Еще раз — в юнити mono 2.6.3 до сих пор

Уверен, что не я. Апгрейд компилятора до моно4.4 только планируется в юнити5.5, когда будет — неизвестно, возможно перенесут на 5.6 и тд.

Да какая разница, какая там версия?! Вам что, религия не позволяет взять пару исходников из другой версии?

Зачем я буду это делать и обеспечивать себе будующий геморой с апдейтом, если текущий компилятор не поддерживает сахар с «async / await» в коде? Если без них, то можно все написать и поверх тредпула без лишних плясок.

Затем, что разговор исходно шел именно про возможность замены компилятора, на что вы возразили, что async/await не будет работать.


Так вот — будет, если подложить ему кучку файликов.

Не будет — компилятор делает подмену-сахар инструкций в какие-то системные классы, аналогично работает синтсаксис linq. Нельзя заставить текущий компилятор обрабатывать ключевые слова «async / await» в коде. Если писать код в VS с обновленным компилятором, то возникнет проблема с strong-name типами (Task и прочими) — они должны будут лежать в сборке именно с тем именем / неймспейсом и подписью, что и в VS. Про подпись не уверен (моно в юнити вроде наплевать на это), но с полным путем нужных типов будут проблемы.
Я компилил async / await для .NET 2.0

Я это правда делал. С именем сборки никакой проблемы не возникло — компилятор его не проверяет. Совсем не проверяет.

Если использовалась внешняя сборка с подписью, то ее имя и паблик-кей еще вроде, не уверен, нужно проверять.

Использовалась кем? Вы вообще читаете что я пишу?


Я компилил async / await для .NET 2.0

Откуда я в честном .NET 2.0, по-вашему, брал внешнюю сборку с подписью? Особенно учитывая, что вместо стандартных реализаций я в тот раз использовал свой велосипед?

PS могу даже привести список классов, которых достаточно чтобы компилятор смог скомпилировать любой код, использующий async/await:


  1. System.Threading.Tasks.Task
  2. System.Threading.Tasks.Task<>
  3. System.Runtime.CompilerServices.AsyncVoidMethodBuilder
  4. System.Runtime.CompilerServices.AsyncTaskMethodBuilder
  5. System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>
  6. TaskAwaiter — полный путь и имя не важны, этот объект должен вернуть метод Task.GetAwaiter()

Последние 4 типа можно сделать структурами для экономии памяти.


Разумеется, если копировать их из Mono — придется скопировать еще несколько, которые используются внутри. А из CoreCLR без изменений скопировать и вовсе не получится — там часть плясок вокруг ExecutionContext придется вырезать.

Потому что автор статьи даже не удосужился узнать особенности того, что тестирует. Конструкция "?.Invoke" появилась только в C#6, а в юнити до сих пор mono 2.6.3, что примерно соответствует C#3.5 + default-ы из C#4.
Ну и про «а вдруг оно поменяется из другого потока» — юнити в паблик-апи вся исключительно однопоточная + внутренние проверки на вызов только из основного потока. Если кто-то решил запилить свой код в другом потоке (а это он должен сделать специально), то пусть сам и обеспечивает целостность и непротиворечивость.
Еще автор статьи наверняка бы предложил «оптимизацию» конструкции «var t = a != null? a: b» в «var t = a ?? b», абсолютно не вникая в перегрузку операторов в кастомной реализации моно юнити (жаль, что не встретил, а то было бы совсем замечательно).
Вывод — меньше рекламы своего поделия, больше изучения предмета тестирования.
Вникать в каждый проект у нас, к сожалению, нет никаких сил.
Вникать в каждый проект у нас, к сожалению, нет никаких сил.

Прекратите тогда вводить людей в заблуждение разбором «ошибок», потому что каждый конкретный продукт может содержать особенности реализации, в которые вы не собираетесь вникать, но с удовольствием расскажите, что ваша тулза нашла ошибку. По сути вы делаете себе антирекламу подобными статьями и комментариями.
Если что, если использовать Visual Stdudio и компилировать DLL который засовывается в Unity, то все прекрасно работает.
Почитайте еще раз, что я написал — простой линейный код почти всегда будет работать как надо, но не весь (те же coroutine-ы + всякие волшебные вещи типа принудительного boxing / unboxing enum-ключа Dictionary в foreach) + абсолютно другой код в плане размещения локальных переменных в теле циклов (приходится выносить руками чтобы не сильно падала скорость) и тд и тп.

А что там не так со скоростью?

Моно (который в юнити) плохо оптимизирует такое — не пытается выкидывать ненужное и не пытается выносить определения переменных за скоуп тела цикла. Если руками вынести — получаешь хорошее ускорение.
ну и с enum-ом в качестве ключа — вот такой код будет гадить в память на каждой итерации (специфично только для старых версий моно, в том числе и для юнити):
enum KeyType {
    One,
    Two
}
var dict = new Dictionary<KeyType, int>() {
    { KeyType.One, 1},
    { KeyType.Two, 2},
};
foreach (var pair in dict) {
    Debug.Log(pair.key + " " + pair.value);
}

По вашей ссылке нашел вот такое описание бага: http://www.gamasutra.com/blogs/WendelinReich/20131109/203841/C_Memory_Management_for_Unity_Developers_part_1_of_3.php, где есть вот это уточнение:


[EDIT] Matthew Hanlon pointed my attention to an unfortunate (yet also very interesting) discrepancy between Microsoft's current C# compiler and the older Mono/C# compiler that Unity uses 'under the hood' to compile your scripts on-the-fly. You probably know that you can use Microsoft Visual Studio to develop and even compile Unity/Mono compatible code. You just drop the respective assembly into the 'Assets' folder. All code is then executed in a Unity/Mono runtime environment. However, results can still differ depending on who compiled the code! foreach loops are just such a case, as I've only now figured out. While both compilers recognize whether a collection's GetEnumerator() returns a struct or a class, the Mono/C# has a bug which 'boxes' (see below, on boxing) a struct-enumerator to create a reference type.

Если в переводе на русский язык и коротко — то это баг компилятора, а не рантайма. Замена компилятора устраняет утечку памяти.

Проблема в том, что они не могут менять ни компилятор ни рантайм вот так просто — могут вылезти всякие новые штуки, в линке (см мой пост ниже) на будующий апгрейд в 5.5 об этом сказано. Ну и у них сейчас есть более важная задача — il2cpp, те по сути на версию моно уже наплевать, не наплевать на трансляцию из конкретного байткода в il2cpp.

"У них" — да. Но мне все больше нравится идея использовать другой компилятор для своего проекта, если когда-нибудь буду его делать.

Скоро это будет невозможно — il2cpp будет на всех платформах (десктоп оставили на десерт), т.е. моно в рантайме не будет в принципе.

Если il2cpp будет работать в соответствии со своим названием (преобразование в cpp любого il) — это не будет проблемой.

Это с чего бы наплевать? Моно как рантайм никуда не денется, о чем Юнитеки неоднократно говорили. Исключение составляет лишь iOS, для которой обязательна возможность AOT-сборки в 64 бита, чего Mono не умеет.
Что меня больше напрягает — так это то, что если юнитеки повысят версию C# для рантайма — им придется долго и нудно допиливать фичи новых версий языка в il2cpp, да и то это будет неполноценно, никаких dynamic и прочих плюшек.
Что меня больше напрягает — так это то, что если юнитеки повысят версию C# для рантайма — им придется долго и нудно допиливать фичи новых версий языка в il2cpp, да и то это будет неполноценно, никаких dynamic и прочих плюшек.

Ну с чего бы? Какой бы версии ни был компилятор — на выходе у него будет корректный IL. И если il2cpp оправдает свое название — то он этот самый IL "проглотит" независимо от того кто его нагенерил.


Тот же dynamic в конечном итоге сводится к рефлексии. Если il2cpp умеет рефлексию — сумеет и dynamic.

Толку с IL, если его нечем будет правильно выполнить? IL2CPP это не только конвертер из IL в С++, а и полноценный рантайм, у него своя реализация потоков, маршалинга и большой части стандартных классов, даже банального String. Практически уверен, что Task юнитекам придется реализовывать также самостоятельно.

Task — это высокоуровневая абстракция, которая прекрасно реализуется в чистом управляемом коде. И у нее уже есть реализация в Mono, ничего самостоятельно делать не придется.

il2cpp на ios — основной вроде с 5.1, il2cpp на android — основной с 5.4, il2cpp на webgl (и дальше через emscripten) — основной (с 5.4 webplayer выпилен окончательно).
На моно остались только десктопы, но и они скорее всего будут переведены на il2cpp. Про то, что моно останется — юнитеки говорили только про редактор. Потому и наплевать, лишь бы il2cpp в поном объеме поддерживал трансляцию.
il2cpp на android — основной с 5.4
Где вы видели про *основной*? С него всего лишь сняли пометку «экспериментально». Это одна из двух опций, они одинаково основные пока что. Хотя тут утверждать не буду.
В WebGL другого варианта, кроме IL2CPP, по сути, и нет — не портировать же Моно…

А вот десктопы совершенно точно будут поддерживать как il2cpp, так и Моно в качестве рантайма, и юнитеки это говорили. Пруф можно найти тут в комментах тут:
https://blogs.unity3d.com/ru/2014/05/20/the-future-of-scripting-in-unity/
The Mono JIT will remain available within the Editor and Standalone.
SEBASTIANO & ZIMM there are more details for this topic on the forum. Basically, the thought is IL2CPP will come to Standalone players someday; but it is not a priority and it will not replace Mono but co-exist as a build option.
С него всего лишь сняли пометку «экспериментально»

Так по сути это и сделало его основным — годным для продакшна. еще пара минорных версий фиксов и моно думаю будут помечать как deprecated, чтобы не тянуть 2 рантайма под одну платформу.
Это было бы так, если бы они были 100% взаимозаменяемы. У Моно достаточно преимуществ перед IL2CPP (см. коммент ниже), и при этом нет каких-либо значимых недостатков (по крайней мере, для меня).
У моно есть один существенный недостаток, который с 5.4 превратился в боль для определенной группы людей — это легкая реверсируемость кода. В случае il2cpp это просто невозможно и китайские товарищи теперь будут рыдать.
Это плюс, да. Хотя, IL2CPP никоим образом не помешал сообществу отреверсить Pokemon GO практически полностью :)
так а там точно был il2cpp? не думаю что они прямо прыгнули на 5.4 за пару недель до релиза. Ну и от разбора контента il2cpp не защищает + гораздо проще снифать незащищенный трафик, чем разбирать приложение. Собственно, так и делают сейчас всяких ботов — просто эмуляцией трафика от клиента.
Точно. Лично колупал apk) Да и они могли ведь вполне пользоваться экспериментальной версией от 5.3.
От реверсинга трафика это не спасает, конечно, но сообщество и логику работы разбирало, так что… Само собой, особо никакой защиты il2cpp не дает, просто усложняет все на порядки.
Будь моя на то воля, лично я бы использовал Mono всюду, где это возможно, по трем причинам:
1) Меньше итоговый размер билда.
2) Сборка быстрее на порядок.
3) AOT-компиляция автоматически лишает крутых штук типа Expression и System.Reflection.Emit, полагающихся на JIT-компиляцию.
AOT / il2cpp билды на ios бегают быстрее на более дохлом железе, чем на более мощном, но под андроидом. Да, можно говорить о том, что платформа вся такая замечательная, оптимизированная и т.п., но и статическая типизация делает свое дело.
Ну я и говорю о том, что выбор рантайма зависит от проекта. В навороченной игре с кучей контента и размером билда эдак 200 МБ, пожалуй, il2cpp подойдет. Если же я делаю онлайн-пятнашки, мне даром не нужна прибавка +20 МБ к билду, когда можно всю игру запихать в 15 МБ (минимальный размер apk в 5.4.0 c Моно — примерно 10 МБ).
+20 оно дает в случае fat build, моно в этом же режиме дает +10. Не сильно большая разница, размер все-равно увеличен до 100мб на GP, а в апсторе так вообще 2гб. Другое дело, что сейчас невозможно выпилить всякий мусор типа UnityEngine.UI, UnityEngine.Networking и тп муть, которая никуда не уперлась — это все едет в билд и так же транслируется в il2cpp.
Технически, неиспользуемые сборки участвуют в трансляции, но по факту на выходе трансляции имеем только код, который реально используется в коде. По крайней мере, так должно быть по задумке юнитеков…
Еще автор статьи наверняка бы предложил «оптимизацию» конструкции «var t = a != null? a: b» в «var t = a ?? b», абсолютно не вникая в перегрузку операторов в кастомной реализации моно юнити (жаль, что не встретил, а то было бы совсем замечательно).

А что можно перегрузить в этом выражении? Разве тернарный оператор перегружается? И зачем это может понадобится делать?

Тут проблема в «a != null»: https://blogs.unity3d.com/2014/05/16/custom-operator-should-we-keep-it/
т.е. есть вот такой удобный кейс:
var rb = GetComponent<RigidBody>() ?? gameObject.AddComponent<RigidBody>();

Вот оно будет работать совсем не так, как задумано в «классическом» поведении.

Интересный подход, хотя, как я понял, используется только в редакторе. Так что по сути если бы анализатор указал на это место, то был бы прав. правда, это добавило бы странных глюков при отладке :) Интересно, а почему в Unity не могли переопределить и оператор ???

Так тут проблема в том, что это сделали не для всех, а только для специфических для юнити объектов типа GameObject, Component и тп + перегрузили для них «operator bool». Для штатных простых типов оно работает как задумано, те со строками, например, такое прокатит. Я и говорю — это именно специфика конкретного продукта.

Погодите. Перегрузили же не оператор приведения типа, а оператор сравнения. Это значит, что во время компиляции компилятор знает, что за объект он с null-ом сравнивает. Что мешает компилятору на этапе компиляции посмотреть, что слева от оператора ?? специальный тип и реализовать этот оператор корректно?

Они скорее всего подхачили UnityEngine.Object и все наследники получили вот такое нестандартное поведение. Те внедрение на уровне компилятора было не сильно большое, а возможно и без внедрения обошлось (путем внутренней реализации методов этого типа при инициализации моно-рантайма) — кто знает, об этом могут рассказать только те, у кого есть полные исходники. Но в 5.5 они собираются проапгрейдить компилятор до моно 4.4, причем не полностью: https://docs.google.com/document/d/1vGzfB3gIU4AEYOjseJxjYytisFkwOKRfEzI5Db6_1vQ/edit
Ничего не мешает, в теории. Вот только по факту оператор ?? игнорирует перегрузку операторов сравнения и сравнивает через Object.ReferenceEquals.

Насколько я понял, перегрузка оператора сделана не средством языка C# (где возможно перегружать некоторые операторы), а хаком в компиляторе (иначе непонятно, что там делать в компиляторе, если сравнение и так можно перегрузить). Поэтому мне совершенно непонятно, что мешает сделать хак законченным и хакать также синтаксическую конструкцию ??, у которых тип слева входит в некий магический список классов, использовать один алгоритм сравнения, а для всех прочих — Object.ReferenceEquals.

Перегрузка сделана обычными средствами языка, в этом можно убедиться, открыв UnityEngine.Object декомпилятором.
Проблема не в перегрузке, а в том, что оператор ?? в С#, грубо говоря, не поддерживает перегрузку операторов. И Юнити тут ни при чем.
Все что вы написали — верно. Только я не понял что значит: «Там все еще хуже». Код в виде:
this.unload?.Invoke();

Развернется в:
IL_0001: ldsfld class [mscorlib]System.Action ThirtyNineEighty.Program::Event // считываем 1 раз
IL_0006: dup // дублируем значение
IL_0007: brtrue.s IL_000c
IL_0009: pop
IL_000a: br.s IL_0012
IL_000c: callvirt instance void [mscorlib]System.Action::Invoke()
IL_0011: nop
IL_0012: br.s IL_0014
IL_0014: ret

Что соответствует тому что я написал. За исключением того, что здесь нет дополнительной локальной переменной. А используется дублирование значения записанного на стек, после считывания поля. Но это уже детали, главное что поле считывается 1 раз, а дальше идет работа с локальным значением.

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

О чем вы вообще спорите? Статья-то о тестировании чего? Пишите код в юнити-проекте, потом забирайте сборки из Temp-а и проверяйте, потому что .net компилятор гораздо прогрессивнее и агрессивнее в плане оптимизации, чем древний патченный моно.
Верно, что спецификация не указывает что будет. Но тем не менее, я настою на том, что практически считывание будет происходить 1 раз и в машинном коде.

x86
Event?.Invoke();
01652D90 mov ecx,dword ptr ds:[400AE28h] // считываем 1 раз
01652D96 test ecx,ecx // проверка на налл
01652D98 jne 01652D9B
01652D9A ret
01652D9B mov eax,dword ptr [ecx+0Ch] // достаем копию
01652D9E mov ecx,dword ptr [ecx+4]
01652DA1 call eax
01652DA3 ret


x64
00007FFE92E44CE0 sub rsp,28h
00007FFE92E44CE4 mov rcx,21B1DE44C20h // считываем 1 раз
00007FFE92E44CEE mov rcx,qword ptr [rcx]
00007FFE92E44CF1 test rcx,rcx // проверка на налл
00007FFE92E44CF4 jne 00007FFE92E44CFB
00007FFE92E44CF6 add rsp,28h
00007FFE92E44CFA ret
00007FFE92E44CFB mov qword ptr [rsp+20h],rcx // достаем копию
00007FFE92E44D00 mov rcx,qword ptr [rcx+8]
00007FFE92E44D04 mov rax,qword ptr [rsp+20h]
00007FFE92E44D09 call qword ptr [rax+18h]
00007FFE92E44D0C nop
00007FFE92E44D0D add rsp,28h
00007FFE92E44D11 ret


И мне совершенно не понятно, почему Вы ко мне прицепились, ведь я всего лишь указал, что это выражение не будет преобразовано в 1 команду. И пояснил, что будет практически, а не теоретически.

Вот такие же "практики, а не теоретики" и попадаются теперь на постоянных UB в C++...

Странно что «Неиспользуемые аргументы при форматировании строки» позиционируется как средний или высокий уровень предупреждения.
Как по мне то тут ничего страшного нет. Падать не будет. Неправильного поведения тоже не ожидается.
Уровень у нас, это не только критичность ошибки, но и её достоверность. Конечно, это два совершенно разных понятия. Но приходится как-то объединять эти две разные характеристики в одну. Двухмерную шкалу измерения ошибки пользователи не оценят :). Здесь высокая достоверность, что это ошибка. Вот и первый уровень.
Здравствуйте. Было бы интересно узнать как обстоят дела с кодом у OGRE 3D.
Да, возможно, кому-то как и мне будет радостно узнать, что актуальные версии редактора есть под linux.
Пробовал запускать демки на Linux Mint, очень круто, просто дальше их сайта никогда не заглядывал.
Зарегистрируйтесь на Хабре, чтобы оставить комментарий