Сообщение Re[19]: А чего молчим про Crowdstrike от 30.07.2024 5:00
Изменено 30.07.2024 5:09 Sinclair
Re[19]: А чего молчим про Crowdstrike
Здравствуйте, vdimas, Вы писали:
V>Для проверки общего случая достаточно было задать динамически размеры массива и область итерирования:
Приведённый пример порождается для случая динамического размера массива и области итерирования.
V>И получишь две проверки в цикле для массива, как и ожидается:
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.
Разница там — в семантике. Для семантически эквивалентных вариантов можно было бы говорить о разнице в эффективности, но её по факту нет (хотя всего два поста назад вы намекали на обратное).
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>
Отож. Как думаете, почему так? И сколько будет проверок, если вместо вызова 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.
Разница там — в семантике. Для семантически эквивалентных вариантов можно было бы говорить о разнице в эффективности, но её по факту нет (хотя всего два поста назад вы намекали на обратное).
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.
Разница там — в семантике. Для семантически эквивалентных вариантов можно было бы говорить о разнице в эффективности, но её по факту нет (хотя всего два поста назад вы намекали на обратное).