Обновить
-3

Пользователь

0,2
Рейтинг
Отправить сообщение

Не надо добавлять if-ы, чтобы избежать немного арифметики. Вот главное, что нужно

Даже не вспоминаем о возможном "лишнем" обращении к незакешированной памяти для обеспечения чистой арифметики, с этим и так понятно.

Но вот у меня есть живой пример, в котором с помощью ИФа можно ИНОГДА избавиться от простого int64 idiv над операндами, гарантированно лежащими в регистре и кеш-линии. И оно того стоит - любые бенчмарки и реальность это подтверждают.

Так что не всякая арифметика одинаково полезна.

Однако если мы говорим не про лабораторный эксперимент с использованием ГСЧ, как правило, эти последовательности не берутся из воздуха. Они хранятся в какой-нибудь базе и уже упорядочены.

Далеко не всегда хранятся, далеко не всегда в базе, и даже если и упорядочены - далеко не всегда по тому критерию(ям), по которому(ым) у нас условие(я).

?: оптимизируется лучше чем if?

Зависит от компилятора/JITа, но на уровне команд процессоров Интела, вполне может компилироваться как бранчлесс.
Однако тут есть нюанс: сначала оба операнда читаются из памяти, потом один из них выбирается. Если чтение не из кешей - то бранч может оказаться выгоднее.

Отличный вопрос, меня он тоже тревожит. По опыту - нет, хотя объяснить я себе этого не могу.
Но: с обратным циклом и пойнтер-арифметикой (ref var curr + MemoryMarshal + Unsafe.Add) можно и от регистра для exit condition избавиться, и доступ прямой последовательный сохранить. И JIT именно так и делает, когда решает, что может поменять направление счётчика.

справедливости ради в этом примере оба массива практически с гарантией окажутся в кешах.

Есть. Даже c# код под Интел экономит регистр на обратных циклах.

Типа такого будет:

L0027	dec	r8d
L002a	jns	short L0020

Кстати, для пущего улучшения, можно циклы перевернуть, чтоб шли от тысячи к нулю. На RISC-платформах сэкономится регистр

На х86/64 тоже, чего бы ему не сэкономить.

Бывает.

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

А пока не упёрся - что бывает в 99% случаев - отличный прозрачный safeguard.

И как предлагаете IList и ReadOnlyCollection приводить к спану?

Есть прикол, что компилятор может неявно создавать копии readonly структур, чтобы гарантировать их имутабельность. 

Такого прикола нет, есть прикол с не-readonly структурами в readonly контексте.

Читайте документацию, она прикольная.

Мы же не пишем сортировку руками, а применяем OrderBy

Вообще-то, "мы" ещё как пишем, потому, что OrderBy - это куча аллокаций, да ещё и приведение к IEnumerable<> со всеми вытекающими.

"Мы" и поиск, случается, пишем, и вообще много всего, что есть в стандартной библиотеке.

Так что "мы" разные.

У ArrayPool.Shared достаточно низкий лимит количества массивов в пуле.
Проверьте, возможно у вас одномоментно требуется сверх того и пул продолжает выделять и выбрасывать слишком много инстансов.

Если так - стоит попробовать использовать ArrayPool.Create со своими настройками. Но он - ConfigurableArrayPool заметно медленнее в многопоточном high throughput. Нужно будет отбенчмаркать.

В моём случае переход со стандарного ConfigurableArrayPool на самописную адаптацию стандартного же SharedArrayPool дал очень ощутимый прирост. Но я микро и нано секунды ловлю, на уровне миллисекунд может и не заметно разницы в скорости.

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

Валидация тяжелая, 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(...)) { ... } - зачем строить такую логику на исключениях?
Внешние неконтролируемые зависимости внутри цикла? - переписать согласно предыдущей рекомендации, прогнать нагрузочные - если всё ещё неприемлемо - заменить зависимости на самописные/более подходящие.

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

Информация

В рейтинге
3 114-й
Зарегистрирован
Активность