Обновить
-3

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

Отправить сообщение

Векторизировать условное поэлементное копирование?

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

То есть, здесь только разработчик может знать, что значения. к примеру, отсортированы - и тогда нужен ИФ с ранним выходом, или "почти отсортированы", и тогда код как в примере будет оптимальным, или вообще непредсказуемые, и тогда нужен оптимизированный вариант из примера.
Тут только JIT PGO мог бы по ходу дела переписывать асм, вопрос - не было бы это дороже, чем прогонять цикл как написано.

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

smlen = 0;for (int i = 0; i < 1000; i++) { if (numbers[i] < 500) { small_numbers[smlen] = numbers[i]; smlen += 1; }}

А о чём конкретно в случае изначального примера должен думать разработчик компилятора?

И что в нём такого, что его нужно прям анализировать и предсказывать [особенно Инженеру встраиваемых систем (младшему)]?

Не надо добавлять 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#. Понятно зачем и почему - но всё равно хотелось бы ещё чуть больше свободы.

Информация

В рейтинге
Не участвует
Зарегистрирован
Активность