Search
Write a publication
Pull to refresh
-2
0.1
Send message

Такими темпами мы сейчас перепишем вашу систему на .НЕТ и отпрофилируем заодно.

Валидация тяжелая, IsValid не возвращает деталей - ок, усложним по-быстрому, будем кешировать и переиспользовать известные детали/проблемы процессинга:

struct ItemProcessingContext
{
  // Содержит поля и флаги, в которых мы можем сохранять
  // результаты различных этапов процессинга записи
}


...

  
ItemProcessingContext context = default;
if (!TryProcess(item, ref context)) Postpone(item, ref context);


или


ItemProcessingContext context = default;
if (IsValid(item, ref context)) Process(item, ref context);
else Postpone(item, ref context);


или


ItemProcessingContext context = default;
if (TryGetStrategy(item, ref context, out var strategy))
{
  strategy.Process(item, ref context);
}
else
{
  // внутри, возможно, будет throw. А может и нет.
  ProcessUnexpectedCase(item, ref context);
}

// если дошли сюда - можем проанализировать накопленный контекст
// и сделать ещё что-нибудь, если вдруг нужно
PostProcessContext(item, ref context);

Ну и не забываем, что каждый уровень должен решать возникающие проблемы в рамках своей ответственности, всё неожиданное выбрасывать наверх.

БД не сумела сохранить данные? - ретрай и если снова не удалось - исключение.
Кэш уже содержит запись для ключа? - молча игнорирует.
А другая имплементация, возможно, кидает DuplicateKeyException.

И так далее.

Сути не меняет:
Если у нас есть известный случай и мы его поддерживаем - нормальная его обработка проходит без исключений.
Неизвестный случай или ошибка процессинга, о котором мы знаем, как сообщить на уровень выше (наш контракт имеет необходимые для этого свойства/типы) - нормальный процессинг, не знаем, как сообщить наверх - исключение.

Доведём мой подход до абсолюта: "идеал" по умолчанию - "все" методы возвращают void, со своими проблемами справляются сами, выполненный метод означает успех, иначе - исключение, означает нерешенную проблему. Простой линейный контрол флоу.

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

Так, ну раз пошла такая пьянка, то давайте уже и я отвечу взаимностью и разверну немного свою т.з.

Перво-наперво - теги у статьи .NET/C#, с этих позиций я и выступаю, понимаю, что могут быть рантаймы/архитектуры, где всё по-другому.

Повторю, да, throw в .NET достаточно/относительно тяжелый, нужно это учитывать, но при правильном применение - более чем годный.
Правильное применение - это не пытаться использовать исключения как оператор ветвления.

Дальше, как я вижу предложенный пример с "миллионами записей, некоторые из них нужно отложить".
На примитивном уровне - это два варианта данных, причём для обоих у меня есть алгоритм обработки. Поэтому код выглядел бы как-то так:

if (!TryProcess(item)) Postpone(item);

или

if (IsValid(item)) Process(item);
else Postpone(item);

А вот если Process или Postpone не справились - то они выкидывают исключение, которое улетает выше - и там уже, возможно, мы решаем что с этим делать.
Потому, что это явно случай, к которому мы не готовы, не важно по какой причине, но что делать мы на этом уровне не знаем.
(кстати, TryProcess хоть и не должен бы, но тоже может выкинуть исключение, если там совсем что-то непредвиденное)

А на уровне выше, может и знаем - например, пропускаем эту запись и идём дальше (как я описывал выше), а может и вообще всё рушим и перезапускаем машину ;)

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

if (TryGetStrategy(item, out var strategy))
{
  strategy.Process(item);
}
else
{
  throw new UnexpectedDataException(item);
}

Кстати, выбранная стратегия тоже может выкинуть исключение, если она по какой-либо причине не справилась, логика выше по стеку остаётся такой же, как и была.

Иначе говоря: поддерживать все известные случаи, в неизвестном - выкидывать исключение, но таких случаев должно оставаться как можно меньше.

Как-то так.

Вот. А дальше? Данные невалидны. Что дальше? Вернуть какой-то result или бросить исключение через throw? Вот о чем разговор.

В общем случае так: если у меня есть обработчик для такого [невалидного] состояния - вызвать его, иначе - исключение, всё прерывается, исключение улетает выше - может быть там знают, что с ним делать.

В случае с параметрами метода если не хотим дефенсив копи делаем параметр ref readonly вместо in.

Это ошибка, причём потенциально весьма дорогая: поведение in и ref readonly с точки зрения defensive copy - идентичное, копия создаётся.

Зашита от defensive copy - readonly struct, прямое чтение полей, пометки методов/пропертей как readonly или магия Unsafe.AsRef.
Ну, или передача как ref-параметр.
Других вариантов нет.

...Да, и вот кстати: в C# использую goto не так, чтобы часто, но регулярно - сильно упрощает жизнь в некоторых случаях, часть из которых уже упомянули.

Считаю его незаменимым инструментом, хоть и излишне "задушенным" в C#. Понятно зачем и почему - но всё равно хотелось бы ещё чуть больше свободы.

А что вы строите на исключениях? 99% "ошибок" - это логика, требующая обработки.

В моём понимании (и практике) ровно наоборот - условные "99% ошибок" внутри процессинга - это прерывающие исключения.
Всё, что не прерывает - это нормальный случай, он выбирается по условиям/анализу/валидации параметров-данных. Или тот самый Try*-pattern.

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

Одноразовые (per API call/button press) проверки - вообще, как правило, не влияют на картину throughput, могут быть построены как угодно (до известной степени, конечно).

...если что - обработка миллиардов итераций per-API-call (в том числе с необходимостью обработки исключений на горячем пути) - специфика моих систем в последние долгие уже годы, так что "цену" разных мелочей я прочувствовал и понимаю. И как показывает моя практика - оптимизация нормального/expected случая "весит" куда больше, чем возможные исключения.

Как вы себе представляете работу try/catch без throw где-то внутри?

Не "без throw где-то внутри", а "без try-catch внутри длинного цикла".
Запросто, try-catch снаружи цикла, если нужно прервать цикл при исключении. Если нужно "пропускать" "исключительные" итерации и продолжать дальше - внешний while/do-while/или-даже-for, внутри подготовка условий и контекста для внутреннего цикла, try-catch, внутри него - внутренний цикл по подготовленным условиям и контексту.
Вариантов масса.

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

И таких "исключительных" записей ожидается больше, чем погрешность от общего количества? - тогда это выглядит не как исключительный случай, а как вариант нормы - здесь просится валидация данных итерации и выбор одного из путей обработки или условный if (!TryProcess(...)) { ... } - зачем строить такую логику на исключениях?
Внешние неконтролируемые зависимости внутри цикла? - переписать согласно предыдущей рекомендации, прогнать нагрузочные - если всё ещё неприемлемо - заменить зависимости на самописные/более подходящие.

В любом случае, этот пример - хорошая иллюстрация использования неподходящей техники, а не ущербности исключений.

начнут строго спрашивать за производительность

try-catch-finally/using практически бесплатны (если не помещать их внутри длинного цикла на горячем пути, конечно)

throw дорогой, но - можно пример сценария, при котором throw начинает влиять на производительность нормального/expected случая, а значит и среднюю производительность?

избегать большой вложенности вызовов (и большой глубины стека) - это тоже хороший стиль

Более чем спорно

Да, был не прав, задумался о своём.

Конечно же, и поля не даёт напрямую изменить, и сеттер для проперти вызвать.

Термин defensive copy применительно к не-ридонли структурам о чём-нибудь говорит?

Вообще-то - нет, не избегает. Ровно такое же поведение с т.з. defensive copy.

Важно, что LinkedList выделяет в куче индивидуальные ноды, тогда так List - массивы (с запасом).

Если списки не настолько большие, чтобы перейти в LOH, то начиная с определённого момента затраты на GC и "утрамбовку" памяти для списка могут стать гораздо дешевле, чем для связного списка.

Не знаю как в вашем случае, а для какого-нибудь хайлоада/хай срупута я бы предпочел список (если быть до конца точным - структуру, основанную на массивах и не допускающую попадания в ЛОХ)

На мой взгляд Zero-Alloc LINQ можно было бы реализовать 100% безопасным кодом

Есть опыт?

Я бы лично с удовольствием (и благодарностью) ознакомился, особенно под .НЕТ ниже седьмого.

Тут есть очень много "но".

Демонстрационный пример очень удачный, так как у нас простой фаст-трак/хэппи пас и несколько обработчиков исключительных ситуаций, куда мы выпадаем редко и передаём в них минимум параметров.

Теперь представим себе, что основная логика и она же - хэппи пас у нас реально объёмная. Она может быть простая, ветвлений минимум, фаст трак (проверки условий, после которых в 99% мы просто переходим на следующую инструкцию) - но объём метода в целом сотни строк, если не тысяча/чи.

Допустим, мы хотим сделать его менее врайт-онли и поделить на более короткие методы с ограниченными ответственностями. Ок, но теперь в каждый выделенный метод мы должны передавать весь необходимый контекст - что запросто может оказаться десятком параметров. А некоторые параметры у нас, к примеру, относительно объёмные структуры. Ок, передаём их по ссылке. Но, теперь у нас ещё и локальность данных размазалась по стеку.

Не уверен, что джит всё это заинлайнит и вернёт наш исходный фаст трак, даже если повесить на каждый метод агрессив инлайнинг.

Ну, как не уверен? Эмпирически знаю - не заинлайнит.

В итоге мы поделили наш фаст трак на несколько вызовов-возвратов, к каждому из которых добавляется копирование всех нужных параметров.

И ради чего? Читабельность? Это, конечно, хорошо, но не всегда стоит потерь производительности.

И это только первое из многих "но" :)

Я к чему? Без бенчмарков и профилирование на разных сценариях я лично никаких сложных и объёмных методов на горячем пути разделять не советовал бы.

...объединять, кстати, на моём опыте - менее рисковано.

Нет, массив всегда есть последовательный непрерывный блок памяти.

Оператор fixed просто запрещает GC перемещать блок памяти (на время своего действия).

А как ключевое слово - позволяет объявить инлайн-буфер заданной длины внутри unsafe-структуры.

Когда вы обрабатываете 10 байт это одно а когда 10 гигабайт это другое.

Когда 10 байт лежат рядом в текущей кэш-линии - это одно, а когда их нужно прочитать непосредственно из DRAM - это совсем другое.

Ключевое слово здесь "не нужен доступ по индексу". На первый взгляд можно бы и согласиться - добавление, вставка и удаление будут быстрее.

Но есть нюансы.

Про кэш-линии уже упомянули.

Но моё любимое - GC.
Мусора такой список создаёт слишком много, чтобы быть быстрым [на более-менее долгом промежутке времени].
Я лично бенчмаркал все стандартные не-массив-бейсед коллекции (включая LinkedList, SortedList и SortedSet), и могу сказать, что для хайлоада они слишком много мусорят. Те, что tree-based - даже при енумерации.

Для себя нашел выход: если действительно не нужен доступ по индексу (и байнари сёрча и т.п.) - лучший вариант это связанный список, но не создающий ноду в куче на каждый элемент, а использующий struct ноды в предварительно выделенных массивах. Да, есть оверхед для учёта и переиспользования свободных/освободившихся слотов в массивах нод, оверхэд на выделение большего массива и копирование данных при добавлении сверх текущей ёмкости, но в целом это гораздо лучший вариант для списков, которые живут дольше одной-двух сборок мусора.

Было бы очень любопытно узнать больше деталей.

Тоже писал некоторым-образом-виртуальную-машину, правда входом для неё был не код, а граф датафлоу. Было бы интересно сравнить опыт.

Хозяева решили закрыть принадлежащее им предприятие.

Ну, вчера хозяева, завтра - никто.
Доля в собственности не отменяет необходимости порядочно себя вести.

Ну, как-бы кидок, не?
После попытки кидка любая реакция считается в пределах нормы.

Технично. Всё правильно сделали.

Любопытно, а если бы... европейцы не поделились никакими доступами - получилось бы добиться такого же результата?

И ещё, почему сразу не отрубили все внешние доступы? - я подозреваю, что подвох в любой момент был ожидаем, как решились дожидаться шагов с той стороны?

...не понимаю праведного возмущения по поводу отжима. Как-бы вторая сторона сама сделала для этого всё возможное.

1
23 ...

Information

Rating
9,150-th
Registered
Activity