Информация об изменениях

Сообщение Re[19]: А чего молчим про Crowdstrike от 30.07.2024 5:00

Изменено 30.07.2024 5:09 Sinclair

Re[19]: А чего молчим про Crowdstrike
Здравствуйте, vdimas, Вы писали:

V>Для проверки общего случая достаточно было задать динамически размеры массива и область итерирования:

Приведённый пример порождается для случая динамического размера массива и области итерирования.

V>И получишь две проверки в цикле для массива, как и ожидается:

V>
V>G_M27646_IG16:  ;; offset=0x010B
V>       cmp      r14d, dword ptr [rbx+0x08]
V>       jae      SHORT G_M27646_IG23
V>       mov      edi, r14d
V>       mov      edi, dword ptr [rbx+4*rdi+0x10]
V>       call     [System.Console:WriteLine(int)]
V>       inc      r14d
V>       cmp      r14d, r15d
V>       jl       SHORT G_M27646_IG16
V>

Отож. Как думаете, почему так? И сколько будет проверок, если вместо вызова Console.WriteLine поставить что-то типа s+=array, как в исходных примерах?

V>И почему ж ты сам не поисследовал тот код?

Я-то как раз поисследовал. Тем более, что я понимаю, что и почему делает JIT.

V>Я вот поисследовал под 8-м дотнетом и увидел забавное — под линухами JIT сгенерил сразу два цикла — в первый ветвление для случая, когда диапазон заведомо попадает, а во второй — когда не попадает, но итерирование там происходит до момента выхода за границу диапазона.

Именно про это вам писал коллега rameel — "loop cloning".
V>Причём, это простейший сниппет.
В этом "простейшем сниппете" вы сравниваете два семантически различных фрагмента кода. Если переписать код так, чтобы фрагменты были семантически эквивалентны, то опять получится одна проверка в цикла.


V>В более общем случае бывают еще выходы из цикла по некоему прикладному условию, бывают оперирования переменной цикла i и т.д.

Бывают. И во всех случаях семантически эквивалентный код порождает эквивалентный asm.

V>>>Теперь-то понятно, зачем сначала рекомендуется брать Span от массива и затем итерироваться?

S>>Покамест нет. Проясните, зачем.

V>Итого, даже для простейшего случая выгодней сначала получить Span, а потом итерировать, чтобы не получать после JIT два варианта цикла (два варианта не могут быть бесплатнее одного) и чтобы выбрасывать исключение не после частичного итерования массива, а до.

Ну так JIT за вас не может решить "а давай-ка я буду выбрасывать исключение [i]до
того, как будут выполнены Console.WriteLine". Это в С++ UB прямо разрешает компилятору такие вещи — если операция с undefined behavior случается, то это позволяет "произволить" не только результат самой этой операции, но и результаты предшествующих ей операций. А в дотнете стандарт устроен по-другому.

V>Т.е. в указанном виде через array.AsSpan(range) сразу же произойдёт проверка диапазонов, что добавляет происходящему элегантности — ведь это теперь не надо делать вручную самому, как оно было принято за правило хорошего тона до эпохи Span.

Дело не в правиле хорошего тона. Дело в поведении кода. Если у вас по ТЗ нужно сначала вывести все элементы массива, попадающие в заказанный диапазон, и потом выбросить ArrayOutOfBounds, то вы так и пишете, и JIT соответствующим образом это обрабатывает. А если вам по ТЗ нужно сначала убедиться, что заказанный диапазон безопасен, и выкинуть исключение в противном случае, то код без проверки до цикла является ошибкой реализации.

V>И третья причина — цикл foreach выглядит элегантнее цикла for со счётчиком.

V>Этот аргумент является совсем уж вкусовщиной и прочими позывами в сторону "избегания синтаксического оверхеда", но, таки, убирание из цикла логики оперирования самим циклом несколько облегчает код, делает его более выразительным. Я такие вещи только приветствую.

V>Таким образом, всем коллегам настоятельно рекомендую взять за практику итерировать Span-ы, а не массивы.

V>Абсолютно безопасный вариант указан мною в сниппете выше.
Непонятно, почему вы называете его "абсолютно безопасным". Все приведённые варианты — абсолютно безопасны в том смысле, что там нет ни UB, ни вывода мусора, ни реинтерпретации памяти, ни риска segfault из-за попытки обращения за пределы отмапленного адресного пространства. Программа абсолютно безопасным способом выполняет то, что ей предписано, а в пограничных случаях абсолютно безопасно выкидывает AOOBE.
Разница там — в семантике. Для семантически эквивалентных вариантов можно было бы говорить о разнице в эффективности, но её по факту нет (хотя всего два поста назад вы намекали на обратное).
Re[19]: А чего молчим про Crowdstrike
Здравствуйте, vdimas, Вы писали:

V>Для проверки общего случая достаточно было задать динамически размеры массива и область итерирования:

Приведённый пример порождается для случая динамического размера массива и области итерирования.

V>И получишь две проверки в цикле для массива, как и ожидается:

V>
V>G_M27646_IG16:  ;; offset=0x010B
V>       cmp      r14d, dword ptr [rbx+0x08]
V>       jae      SHORT G_M27646_IG23
V>       mov      edi, r14d
V>       mov      edi, dword ptr [rbx+4*rdi+0x10]
V>       call     [System.Console:WriteLine(int)]
V>       inc      r14d
V>       cmp      r14d, r15d
V>       jl       SHORT G_M27646_IG16
V>

Отож. Как думаете, почему так? И сколько будет проверок, если вместо вызова Console.WriteLine поставить что-то типа s+=array[i], как в исходных примерах?

V>И почему ж ты сам не поисследовал тот код?

Я-то как раз поисследовал. Тем более, что я понимаю, что и почему делает JIT.

V>Я вот поисследовал под 8-м дотнетом и увидел забавное — под линухами JIT сгенерил сразу два цикла — в первый ветвление для случая, когда диапазон заведомо попадает, а во второй — когда не попадает, но итерирование там происходит до момента выхода за границу диапазона.

Именно про это вам писал коллега rameel — "loop cloning".
V>Причём, это простейший сниппет.
В этом "простейшем сниппете" вы сравниваете два семантически различных фрагмента кода. Если переписать код так, чтобы фрагменты были семантически эквивалентны, то опять получится одна проверка в цикла.


V>В более общем случае бывают еще выходы из цикла по некоему прикладному условию, бывают оперирования переменной цикла i и т.д.

Бывают. И во всех случаях семантически эквивалентный код порождает эквивалентный asm.

V>>>Теперь-то понятно, зачем сначала рекомендуется брать Span от массива и затем итерироваться?

S>>Покамест нет. Проясните, зачем.

V>Итого, даже для простейшего случая выгодней сначала получить Span, а потом итерировать, чтобы не получать после JIT два варианта цикла (два варианта не могут быть бесплатнее одного) и чтобы выбрасывать исключение не после частичного итерования массива, а до.

Ну так JIT за вас не может решить "а давай-ка я буду выбрасывать исключение до того, как будут выполнены Console.WriteLine". Это в С++ UB прямо разрешает компилятору такие вещи — если операция с undefined behavior случается, то это позволяет "произволить" не только результат самой этой операции, но и результаты предшествующих ей операций. А в дотнете стандарт устроен по-другому.

V>Т.е. в указанном виде через array.AsSpan(range) сразу же произойдёт проверка диапазонов, что добавляет происходящему элегантности — ведь это теперь не надо делать вручную самому, как оно было принято за правило хорошего тона до эпохи Span.

Дело не в правиле хорошего тона. Дело в поведении кода. Если у вас по ТЗ нужно сначала вывести все элементы массива, попадающие в заказанный диапазон, и потом выбросить ArrayOutOfBounds, то вы так и пишете, и JIT соответствующим образом это обрабатывает. А если вам по ТЗ нужно сначала убедиться, что заказанный диапазон безопасен, и выкинуть исключение в противном случае, то код без проверки до цикла является ошибкой реализации.

V>И третья причина — цикл foreach выглядит элегантнее цикла for со счётчиком.

V>Этот аргумент является совсем уж вкусовщиной и прочими позывами в сторону "избегания синтаксического оверхеда", но, таки, убирание из цикла логики оперирования самим циклом несколько облегчает код, делает его более выразительным. Я такие вещи только приветствую.

V>Таким образом, всем коллегам настоятельно рекомендую взять за практику итерировать Span-ы, а не массивы.

V>Абсолютно безопасный вариант указан мною в сниппете выше.
Непонятно, почему вы называете его "абсолютно безопасным". Все приведённые варианты — абсолютно безопасны в том смысле, что там нет ни UB, ни вывода мусора, ни реинтерпретации памяти, ни риска segfault из-за попытки обращения за пределы отмапленного адресного пространства. Программа абсолютно безопасным способом выполняет то, что ей предписано, а в пограничных случаях абсолютно безопасно выкидывает AOOBE.
Разница там — в семантике. Для семантически эквивалентных вариантов можно было бы говорить о разнице в эффективности, но её по факту нет (хотя всего два поста назад вы намекали на обратное).