All streams
Search
Write a publication
Pull to refresh
20
0
Send message
В 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), и наверняка еще много раз до «изобретения» трансдьюсеров.
Для сравнения, вот во что JIT превращает тот же код, если отладчик прицеплен с самого начала:
00007FFA38F54014  mov         qword ptr [rsp+20h],0  
00007FFA38F5401D  mov         dword ptr [rsp+28h],0  
00007FFA38F54025  jmp         00007FFA38F54079  
00007FFA38F54027  mov         rcx,0B42D95E728h  
00007FFA38F54031  mov         rcx,qword ptr [rcx]  
00007FFA38F54034  mov         rax,qword ptr [rsp+20h]  
00007FFA38F54039  mov         qword ptr [rsp+30h],rax  
00007FFA38F5403E  mov         qword ptr [rsp+38h],rcx  
00007FFA38F54043  mov         rax,qword ptr [rsp+38h]  
00007FFA38F54048  cmp         byte ptr [rax],0  
00007FFA38F5404B  mov         rcx,qword ptr [rsp+38h]  
00007FFA38F54050  mov         edx,dword ptr [rsp+28h]  
; вызов get_Item(int32)
00007FFA38F54054  call        00007FFA9743F430  
00007FFA38F54059  mov         dword ptr [rsp+40h],eax  
00007FFA38F5405D  movsxd      rcx,dword ptr [rsp+40h]  
00007FFA38F54062  mov         rax,qword ptr [rsp+30h]  
; totalValue += element
00007FFA38F54067  add         rax,rcx  
00007FFA38F5406A  mov         qword ptr [rsp+20h],rax  
00007FFA38F5406F  mov         eax,dword ptr [rsp+28h]  
00007FFA38F54073  inc         eax  
00007FFA38F54075  mov         dword ptr [rsp+28h],eax  
00007FFA38F54079  mov         rcx,0B42D95E728h  
00007FFA38F54083  mov         rcx,qword ptr [rcx]  
00007FFA38F54086  mov         eax,dword ptr [rsp+28h]  
00007FFA38F5408A  mov         dword ptr [rsp+44h],eax  
00007FFA38F5408E  mov         qword ptr [rsp+48h],rcx  
00007FFA38F54093  mov         rax,qword ptr [rsp+48h]  
00007FFA38F54098  cmp         byte ptr [rax],0  
00007FFA38F5409B  mov         rcx,qword ptr [rsp+48h]  
; вызов Count и проверка i < Count
00007FFA38F540A0  call        00007FFA9743F470  
00007FFA38F540A5  mov         dword ptr [rsp+50h],eax  
00007FFA38F540A9  mov         eax,dword ptr [rsp+50h]  
00007FFA38F540AD  cmp         dword ptr [rsp+44h],eax  
00007FFA38F540B1  jl          00007FFA38F54027  

IL код не показывает полной картины. Смотрите disassembly:
код на C#:
static long TestListFor()
{
    Debugger.Launch();
    long totalValue = 0;
    for (int i = 0; i < g_IntList.Count; i++)
    {
        totalValue += g_IntList[i];
    }
    return totalValue;
}

IL:
.method private hidebysig static int64  TestListFor() cil managed
{
  // Code size       47 (0x2f)
  .maxstack  3
  .locals init ([0] int64 totalValue,
           [1] int32 i)
  IL_0000:  call       bool [mscorlib]System.Diagnostics.Debugger::Launch()
  IL_0005:  pop
  IL_0006:  ldc.i4.0
  IL_0007:  conv.i8
  IL_0008:  stloc.0
  IL_0009:  ldc.i4.0
  IL_000a:  stloc.1
  IL_000b:  br.s       IL_0020
  IL_000d:  ldloc.0
  IL_000e:  ldsfld     class [mscorlib]System.Collections.Generic.List`1<int32> ForEachTest.Program::g_IntList
  IL_0013:  ldloc.1
  IL_0014:  callvirt   instance !0 class [mscorlib]System.Collections.Generic.List`1<int32>::get_Item(int32)
  IL_0019:  conv.i8
  IL_001a:  add
  IL_001b:  stloc.0
  IL_001c:  ldloc.1
  IL_001d:  ldc.i4.1
  IL_001e:  add
  IL_001f:  stloc.1
  IL_0020:  ldloc.1
  IL_0021:  ldsfld     class [mscorlib]System.Collections.Generic.List`1<int32> ForEachTest.Program::g_IntList
  IL_0026:  callvirt   instance int32 class [mscorlib]System.Collections.Generic.List`1<int32>::get_Count()
  IL_002b:  blt.s      IL_000d
  IL_002d:  ldloc.0
  IL_002e:  ret
} // end of method Program::TestListFor


Реально выполняемый код (кусок от зануления аккумулятора до 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 версий фреймворка назад, отлично это подтверждает. У меня, например, не сошлись. Но я не делаю их этого глобальных выводов.
Ок. тогда статья некорректна только до примечаний переводчика.
тесты TestListFor и TestArrayFor считают сумму индексов. Остальные тесты — считают сумму элементов списка/массива.
static void TestArrayFor()
{
    long totalValue = 0;
    for (int i = 0; i < g_IntArray.Length; i++)
        totalValue += i;
}

static void TestListFor()
{
    long totalValue = 0;
    for (int i = 0; i < g_IntList.Count; i++)
        totalValue += i;
}


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

мягко выражаясь, некорректен.

List.ForEach на моей машине (с) работает медленее, чем foreach, если totalValue используется после цикла (даже в виде return totalValue)

Микрооптимизация — [почти всегда] зло.
При наследовании придется явно указывать параметры для base [предположительно, если первичные конструкторы когда-нибудь будут добавлены в C#]:
public class Point3(int x, int y, int z) : Point(x, y)
{

}
var x = point?.X; — это синтаксический сахар вместо
var x = point == null ? (int?)null : point.X;

тип x будет int? без всяких ?? справа от выражения.
Конструкцию ?. — вводят для того, чтобы получать null вместо NullReferenceException.
Если тип x будет int, как вы предположили, то весь смысл конструкции потеряется — потому что вести себя она будет точно так же, как point.X.
Еще static type using statements добавили (в статье не упомнянут, есть по ссылкам): using System.Math;

сорри, не заметил
x — это параметер Primary Constructor-а:
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к лет назад) подразумевают под «серией полиморфных предикатов» и как именно это позволило устранить проблемы, которые проявились именно из-за знания матлогики.
Знание математической логики — это знание про предикаты и критерионы, а не умение расставить || и && в if.

Information

Rating
Does not participate
Location
Беларусь
Date of birth
Registered
Activity