В SQL Server Azure / SQL Server 2016 есть механизм Query Store, который гораздо надежнее и точнее ручного сбора данных раз в 5-10 минут. По сути он делает то же самое, что ваши самописные job-ы, но гораздо точнее (на уровне отдельных statement-ов, пишет статистику по каждому выполнению + использованный план), непрерывно, в фоне, не влияя при этом на производительность.
Не в любом. В C# лямбда компилируется или в анонимный метод (не просто работает похожим образом, а реально в него компилируется), или в Expression Tree — композит, который в рантайме можно отобразить в SQL / OData filter / что угодно. Так что C# лямбды притащены ради уменьшения дырки между абстракциями repository / query object и обычными коллекциями объектов в памяти.
Закрыть хэнл, если вдруг он оказался открыт на этапе финализации — это проблемы стрима.
Дело не в том, что вы предлагаете писать или не писать финализаторы. Дело в том, через весь текст статьи проходит мысль «Dispose и Finalize непрерывно связаны, для IDisposable всегда нужно добавлять финализатор».
Т.е. вы вроде и не предлагаете писать финализаторы, но вывод статьи читается вот так:
Прежде чем бросаться добавлять финализаторы для всех классов, реализующих IDisposable, стоит подумать, а действительно ли они так нужны…
Но если вы всё-таки решили использовать финализаторы [для всех классов, реализующих IDisposable, о которых мы тут писали выше], то PVS-Studio… покажет все места в финализаторе, где может возникнуть NullReferenceException [при обращении из финализатора ко вложенным объектам ради вызова у них Dispose].
И вот эти намеки на «есть IDisposable — реализуй финализатор!» продолжаются в комментариях. Вам
Финализатор, в свою очередь, нужен в случае, если вы работаете напрямую с IntPtr.
А вы вроде как и соглашаетесь, но продолжаете намекать на то, что стрим — это неуправляемый ресурс (а неуправляемые ресурсы надо контролировать через финализатор, это все знают!):
Согласен. Но здесь у вас фактически тоже неуправляемый ресурс, только обёрнутый в IDisposable.
Т.е. я то понимаю, как работает Dispose / Finalize. И что есть разница между прямым (IntPtr) и косвенным (IDisposable) владением неуправляемым ресурсом. И вы скорее всего понимаете. И остальные в этом треде понимают. Но это не делает статью лучше, а ворнинг — адекватнее.
Разница в том, что
— голый IntPtr нужно освобождать и в IDisposable.Dispose(), и в финализаторе.
— Dispose у объекта типа Stream нужно вызывать только в Dispose().
То, что Stream внутри использует неуправляемый ресурс — это ваше предположение (вдруг там MemoryStream) и проблемы самого стрима (его финализатор сам должен закрыть хэндл). Писать код на основе предположений — нельзя :) Обращаться к стриму в финализаторе — нельзя (вдуг его финализатор уже отработал). Делать любых предположений о его состоянии — нельзя.
Проверка на null перед обращением спасет чуть более чем никак.
Финализатор предназначен для освобождения неуправляемых ресурсов принадлежащих непосредственно текущему объекту. И больше ни для чего другого. Это, кстати, прямо сказано даже в MSDN:
The Finalize method is used to perform cleanup operations on unmanaged resources held by the current object before the object is destroyed
То, что в финализатор полез по ссылке на другой управляемый объект, скорее всего означает что разработчик не понимает разницы межу Dispose и Finalize, и пытается «освобожать» управляемые ресурсы. Или вообще пытается «помочь» сборщику мусора. Вероятность того, что он на самом деле гуру, и использует финализатор не по назначению сознательно, знает обо всех последствиях своего решения… и при этом не проверил на null — ничтожно мала.
Адекватной реакцией коданализа на обращение к reference-свойству было бы «Ты точно знаешь что делаешь? Остановись, почитай про финализаторы! Если твой класс не контролирует неуправляемые ресурсы напрямую — удали финализатор и живи дальше! Если контролирует — используй SafeHandle!»
Еще более адекватным было бы предупреждение на сам факт наличия финализатора. Со страшными минусами из статьи в описании. С отсылкой к тому же SafeHandle. Это действительно предотвратило бы основную ошибку разработчика — реализацию финализатора в случае, когда он совсем не нужен.
А PVS вместо этого говорит разработчику «Все ок, делай финализатор, лазь в нем по другим объектам — это нормально. На null только проверь, и все будет хорошо!»
Потому что мы не сможем ее подписать, у нас нет закрытого ключа.
Есть стандартный способ правки чужих сборок. Достаточно пересобрать сборку со старым public key (через delay signing) и отключить для нее strong name verification (запуком sn -Vr *,publickey).
В .NET IEnumerable — это стандартный интерфейс чего-то, что можно перебрать по одному элементу. C# позволяет привесить к любому IEnumerable внешний метод (трансдьюсер в терминологии статьи), который вернет новый IEnumerable, при переборе которого вы получите take/where/map/skip от оригинального IEnumerable. Без создания промежуточной коллекции, без ограничения на тип элементов, и со строгой типизацией. Что-то похожее реализовано в F# (sequences), и наверняка еще много раз до «изобретения» трансдьюсеров.
Реально выполняемый код (кусок от зануления аккумулятора до jump на начало списка. Нет ни одного call, выполняется тривиальный цикл по элементам:
; totalValue = 0
00007FFA38F30441 xor ecx,ecx
; for
; проверка i < list.Count и границ внутреннего массива
00007FFA38F30443 mov r9,0BF57335778h
00007FFA38F3044D mov r9,qword ptr [r9]
00007FFA38F30450 mov eax,dword ptr [r9+18h]
00007FFA38F30454 cmp edx,eax
00007FFA38F30456 jl 00007FFA38F30460
00007FFA38F30458 mov rax,rcx
00007FFA38F3045B jmp 00007FFA38F30486
00007FFA38F3045D nop dword ptr [rax]
00007FFA38F30460 cmp edx,eax
00007FFA38F30462 jae 00007FFA38F30495
00007FFA38F30464 mov r9,qword ptr [r9+8]
00007FFA38F30468 movsxd r10,r8d
00007FFA38F3046B mov rax,qword ptr [r9+8]
00007FFA38F3046F cmp r10,rax
00007FFA38F30472 jae 00007FFA38F30490
; загрузка i-го элемента в eax, в IL выглядело как callvirt instance !0 class [mscorlib]System.Collections.Generic.List`1::get_Item(int32)
00007FFA38F30474 mov eax,dword ptr [r9+r10*4+10h]
00007FFA38F30479 movsxd rax,eax
; totalValue += rax
00007FFA38F3047C add rcx,rax
00007FFA38F3047F inc edx
00007FFA38F30481 inc r8d
; jump на начало for
00007FFA38F30484 jmp 00007FFA38F30443
JIT знает о стандартных генериках. Т.е. в IL callvirt есть — а при выполнении его уже нет.
Для проверки лучше цеплять дебаггер уже в процессе, после того, как JIT отрабтал. Иначе получите код без JIT оптимизаций (т.е. медленный и с call).
Практика подтверждает теорию, а не наоборот. На практике, же согласно оригинальной статье, for без оптимизации компилятора работает медленнее List.ForEach.
Цикл for (который написан внутри List.ForEach) находится в mscorlib.dll. Которая, естественно, скопилирована с оптимизацией, и для которой заранее сгерерирован не-отладочный native image. Т.е. тест «без оптимизации» на самом деле сравнивает «for без оптимизации и без bounds check elimination» и «for c вызовом action c оптимизацией». Последний, естественно, оказывается быстрее. Так что практика вполне подтверждает теорию.
Честно неуловил, в чём вы нашли отличие цикла от перебора?
Автор вызывает цикл один раз (и один раз тратит время на создание энумератора, поэтому цена создания энумератора вообще никак не влияет на его результаты). Вы вызываете цикл 1000 раз (или больше), 1000 раз тратите время на создание энумератора, да еще и измеряете затраты на внешний цикл и switch (насколько я понял). Поэтому у вас в левой стороне графика такие сильные расхождения. Ну и опять же — JIT на вашей платформе ведет себя совсем не так, как JIT на платформе автора. Вы сравниваете разные вещи, просто графики получились похожие.
В чём же его спорность
Спорность в том, что сравнивать только средние значения некорректно. Нужно учитывать разброс результатов.
Вот результаты с моей машины (с) на 20 попытках:
ForEach: среднее 367.9, отклонение (оценочное) 13.7. 95% попыток будут в диапазоне 354-381.
foreach: среднее 373.8, отклонение (оценочное) 16.8. 95% попыток будут в диапазоне 356-390.
Средние рядом, разброс значений гораздо больше чем разница средних.
С достаточно большой вероятностью конкретный запуск foreach где-то в приложении будет быстрее (не намного, но быстрее) конкретного запуска ForEach.
В моей выборке есть пара значений с разницей в 39ms (10%!) в пользу foreach. Можно это засчитать как аргумент в пользу спорности?
Это данные для корректного теста.
for естественно будет работать быстрее, чем List.ForEach — но для того, чтобы это доказать, не нужно ничего тестировать и рисовать графики. Достаточно просто заглянуть в исходники List.ForEach.
На вашем скрипте ситуация совсем не та, которую тестирует автор оригинальной статьи — вы тестируете расходы на сам факт запуска цикла. Автор оригинала тестировал скорость перебора элементов. Это две совсем разные метрики (и сняты они на разных платформах). А вы пытаетесь использовать свои результаты как подтверждение оригинальных замеров.
List.ForEach быстрее, чем foreach — очень спорное утверждение. Разница между 381ms и 401ms на 100КК итераций, на практически пустом цикле, настолько незначительна, что при разработке можно спокойно выбирать то, что подходит по семантике — на производительность это не повлияет вообще никак. Практически, разницы нет. А если и есть — то она может качнуться в другую сторону на те же 20ms в зависимости от фазы луны.
У меня как раз все нормально. на 100КК элементах, 20 итерациях, релиз, x64 с оптимизациями.
for — 220ms
List.ForEach — 381ms (который на самом деле for-no-count + вызов метода)
foreach — 401ms
Вот только на другой машине результаты будут совсем другими (скорее всего с перевесом в пользу стандартного for). То, что у вас цифры сошлись с результатами некоректного теста, запущенного кем-то 8 лет и 2.5 версий фреймворка назад, отлично это подтверждает. У меня, например, не сошлись. Но я не делаю их этого глобальных выводов.
var x = point?.X; — это синтаксический сахар вместо
var x = point == null ? (int?)null : point.X;
тип x будет int? без всяких ?? справа от выражения.
Конструкцию ?. — вводят для того, чтобы получать null вместо NullReferenceException.
Если тип x будет int, как вы предположили, то весь смысл конструкции потеряется — потому что вести себя она будет точно так же, как point.X.
public class Point(int x, int y)
{
public int X { get { return x; } }
public int Y { get { return y; } }
public int Dist { get; } = Math.Sqrt(x * x + y * y);
}
Вы подменили понятие матлогики на «знание двух логических операций, которые знает любая мартышка», и тут же переписали код, проявив знание тру-матлогики. И из этого как-то сделали вывод, что именно знание тру-матлогики стало причиной появления изначального спагетти. Как — я не понимаю.
Я не утверждал, что понятие предикат есть только в математической логике. Или что его не было до появления математической логики в виде отдельного раздела математики. В вашем комментарии вы достаточно однозначно использовали предикат как термин из математической логики. Если вы имели ввиду сказуемое, или оригинальный смысл слова предикат, в котором его употреблял Аристотель — то дайте, пожалуйста, пояснение, что лингвисты (или Аристотель 2к лет назад) подразумевают под «серией полиморфных предикатов» и как именно это позволило устранить проблемы, которые проявились именно из-за знания матлогики.
Дело не в том, что вы предлагаете писать или не писать финализаторы. Дело в том, через весь текст статьи проходит мысль «Dispose и Finalize непрерывно связаны, для IDisposable всегда нужно добавлять финализатор».
Т.е. вы вроде и не предлагаете писать финализаторы, но вывод статьи читается вот так:
И вот эти намеки на «есть IDisposable — реализуй финализатор!» продолжаются в комментариях. Вам
А вы вроде как и соглашаетесь, но продолжаете намекать на то, что стрим — это неуправляемый ресурс (а неуправляемые ресурсы надо контролировать через финализатор, это все знают!):
Т.е. я то понимаю, как работает Dispose / Finalize. И что есть разница между прямым (IntPtr) и косвенным (IDisposable) владением неуправляемым ресурсом. И вы скорее всего понимаете. И остальные в этом треде понимают. Но это не делает статью лучше, а ворнинг — адекватнее.
— голый IntPtr нужно освобождать и в IDisposable.Dispose(), и в финализаторе.
— Dispose у объекта типа Stream нужно вызывать только в Dispose().
То, что Stream внутри использует неуправляемый ресурс — это ваше предположение (вдруг там MemoryStream) и проблемы самого стрима (его финализатор сам должен закрыть хэндл). Писать код на основе предположений — нельзя :) Обращаться к стриму в финализаторе — нельзя (вдуг его финализатор уже отработал). Делать любых предположений о его состоянии — нельзя.
Проверка на null перед обращением спасет чуть более чем никак.
То, что в финализатор полез по ссылке на другой управляемый объект, скорее всего означает что разработчик не понимает разницы межу Dispose и Finalize, и пытается «освобожать» управляемые ресурсы. Или вообще пытается «помочь» сборщику мусора. Вероятность того, что он на самом деле гуру, и использует финализатор не по назначению сознательно, знает обо всех последствиях своего решения… и при этом не проверил на null — ничтожно мала.
Адекватной реакцией коданализа на обращение к reference-свойству было бы «Ты точно знаешь что делаешь? Остановись, почитай про финализаторы! Если твой класс не контролирует неуправляемые ресурсы напрямую — удали финализатор и живи дальше! Если контролирует — используй SafeHandle!»
Еще более адекватным было бы предупреждение на сам факт наличия финализатора. Со страшными минусами из статьи в описании. С отсылкой к тому же SafeHandle. Это действительно предотвратило бы основную ошибку разработчика — реализацию финализатора в случае, когда он совсем не нужен.
А PVS вместо этого говорит разработчику «Все ок, делай финализатор, лазь в нем по другим объектам — это нормально. На null только проверь, и все будет хорошо!»
Есть стандартный способ правки чужих сборок. Достаточно пересобрать сборку со старым public key (через delay signing) и отключить для нее strong name verification (запуком sn -Vr *,publickey).
код на C#:
IL:
Реально выполняемый код (кусок от зануления аккумулятора до jump на начало списка. Нет ни одного call, выполняется тривиальный цикл по элементам:
JIT знает о стандартных генериках. Т.е. в IL callvirt есть — а при выполнении его уже нет.
Для проверки лучше цеплять дебаггер уже в процессе, после того, как JIT отрабтал. Иначе получите код без JIT оптимизаций (т.е. медленный и с call).
Цикл for (который написан внутри List.ForEach) находится в mscorlib.dll. Которая, естественно, скопилирована с оптимизацией, и для которой заранее сгерерирован не-отладочный native image. Т.е. тест «без оптимизации» на самом деле сравнивает «for без оптимизации и без bounds check elimination» и «for c вызовом action c оптимизацией». Последний, естественно, оказывается быстрее. Так что практика вполне подтверждает теорию.
Автор вызывает цикл один раз (и один раз тратит время на создание энумератора, поэтому цена создания энумератора вообще никак не влияет на его результаты). Вы вызываете цикл 1000 раз (или больше), 1000 раз тратите время на создание энумератора, да еще и измеряете затраты на внешний цикл и switch (насколько я понял). Поэтому у вас в левой стороне графика такие сильные расхождения. Ну и опять же — JIT на вашей платформе ведет себя совсем не так, как JIT на платформе автора. Вы сравниваете разные вещи, просто графики получились похожие.
Спорность в том, что сравнивать только средние значения некорректно. Нужно учитывать разброс результатов.
Вот результаты с моей машины (с) на 20 попытках:
ForEach: среднее 367.9, отклонение (оценочное) 13.7. 95% попыток будут в диапазоне 354-381.
foreach: среднее 373.8, отклонение (оценочное) 16.8. 95% попыток будут в диапазоне 356-390.
Средние рядом, разброс значений гораздо больше чем разница средних.
С достаточно большой вероятностью конкретный запуск foreach где-то в приложении будет быстрее (не намного, но быстрее) конкретного запуска ForEach.
В моей выборке есть пара значений с разницей в 39ms (10%!) в пользу foreach. Можно это засчитать как аргумент в пользу спорности?
for естественно будет работать быстрее, чем List.ForEach — но для того, чтобы это доказать, не нужно ничего тестировать и рисовать графики. Достаточно просто заглянуть в исходники List.ForEach.
На вашем скрипте ситуация совсем не та, которую тестирует автор оригинальной статьи — вы тестируете расходы на сам факт запуска цикла. Автор оригинала тестировал скорость перебора элементов. Это две совсем разные метрики (и сняты они на разных платформах). А вы пытаетесь использовать свои результаты как подтверждение оригинальных замеров.
List.ForEach быстрее, чем foreach — очень спорное утверждение. Разница между 381ms и 401ms на 100КК итераций, на практически пустом цикле, настолько незначительна, что при разработке можно спокойно выбирать то, что подходит по семантике — на производительность это не повлияет вообще никак. Практически, разницы нет. А если и есть — то она может качнуться в другую сторону на те же 20ms в зависимости от фазы луны.
for — 220ms
List.ForEach — 381ms (который на самом деле for-no-count + вызов метода)
foreach — 401ms
Вот только на другой машине результаты будут совсем другими (скорее всего с перевесом в пользу стандартного for). То, что у вас цифры сошлись с результатами некоректного теста, запущенного кем-то 8 лет и 2.5 версий фреймворка назад, отлично это подтверждает. У меня, например, не сошлись. Но я не делаю их этого глобальных выводов.
Поэтому основной вывод статьи
мягко выражаясь, некорректен.
List.ForEach на моей машине (с) работает медленее, чем foreach, если totalValue используется после цикла (даже в виде return totalValue)
Микрооптимизация — [почти всегда] зло.
тип x будет int? без всяких ?? справа от выражения.
Конструкцию
?.
— вводят для того, чтобы получать null вместо NullReferenceException.Если тип x будет int, как вы предположили, то весь смысл конструкции потеряется — потому что вести себя она будет точно так же, как point.X.
Еще static type using statements добавили (в статье не упомнянут, есть по ссылкам): using System.Math;
сорри, не заметил
Я не утверждал, что понятие предикат есть только в математической логике. Или что его не было до появления математической логики в виде отдельного раздела математики. В вашем комментарии вы достаточно однозначно использовали предикат как термин из математической логики. Если вы имели ввиду сказуемое, или оригинальный смысл слова предикат, в котором его употреблял Аристотель — то дайте, пожалуйста, пояснение, что лингвисты (или Аристотель 2к лет назад) подразумевают под «серией полиморфных предикатов» и как именно это позволило устранить проблемы, которые проявились именно из-за знания матлогики.