А что вы строите на исключениях? 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 случая, а значит и среднюю производительность?
избегать большой вложенности вызовов (и большой глубины стека) - это тоже хороший стиль
Важно, что LinkedList выделяет в куче индивидуальные ноды, тогда так List - массивы (с запасом).
Если списки не настолько большие, чтобы перейти в LOH, то начиная с определённого момента затраты на GC и "утрамбовку" памяти для списка могут стать гораздо дешевле, чем для связного списка.
Не знаю как в вашем случае, а для какого-нибудь хайлоада/хай срупута я бы предпочел список (если быть до конца точным - структуру, основанную на массивах и не допускающую попадания в ЛОХ)
Демонстрационный пример очень удачный, так как у нас простой фаст-трак/хэппи пас и несколько обработчиков исключительных ситуаций, куда мы выпадаем редко и передаём в них минимум параметров.
Теперь представим себе, что основная логика и она же - хэппи пас у нас реально объёмная. Она может быть простая, ветвлений минимум, фаст трак (проверки условий, после которых в 99% мы просто переходим на следующую инструкцию) - но объём метода в целом сотни строк, если не тысяча/чи.
Допустим, мы хотим сделать его менее врайт-онли и поделить на более короткие методы с ограниченными ответственностями. Ок, но теперь в каждый выделенный метод мы должны передавать весь необходимый контекст - что запросто может оказаться десятком параметров. А некоторые параметры у нас, к примеру, относительно объёмные структуры. Ок, передаём их по ссылке. Но, теперь у нас ещё и локальность данных размазалась по стеку.
Не уверен, что джит всё это заинлайнит и вернёт наш исходный фаст трак, даже если повесить на каждый метод агрессив инлайнинг.
Ну, как не уверен? Эмпирически знаю - не заинлайнит.
В итоге мы поделили наш фаст трак на несколько вызовов-возвратов, к каждому из которых добавляется копирование всех нужных параметров.
И ради чего? Читабельность? Это, конечно, хорошо, но не всегда стоит потерь производительности.
И это только первое из многих "но" :)
Я к чему? Без бенчмарков и профилирование на разных сценариях я лично никаких сложных и объёмных методов на горячем пути разделять не советовал бы.
...объединять, кстати, на моём опыте - менее рисковано.
Ключевое слово здесь "не нужен доступ по индексу". На первый взгляд можно бы и согласиться - добавление, вставка и удаление будут быстрее.
Но есть нюансы.
Про кэш-линии уже упомянули.
Но моё любимое - GC. Мусора такой список создаёт слишком много, чтобы быть быстрым [на более-менее долгом промежутке времени]. Я лично бенчмаркал все стандартные не-массив-бейсед коллекции (включая LinkedList, SortedList и SortedSet), и могу сказать, что для хайлоада они слишком много мусорят. Те, что tree-based - даже при енумерации.
Для себя нашел выход: если действительно не нужен доступ по индексу (и байнари сёрча и т.п.) - лучший вариант это связанный список, но не создающий ноду в куче на каждый элемент, а использующий struct ноды в предварительно выделенных массивах. Да, есть оверхед для учёта и переиспользования свободных/освободившихся слотов в массивах нод, оверхэд на выделение большего массива и копирование данных при добавлении сверх текущей ёмкости, но в целом это гораздо лучший вариант для списков, которые живут дольше одной-двух сборок мусора.
Любопытно, а если бы... европейцы не поделились никакими доступами - получилось бы добиться такого же результата?
И ещё, почему сразу не отрубили все внешние доступы? - я подозреваю, что подвох в любой момент был ожидаем, как решились дожидаться шагов с той стороны?
...не понимаю праведного возмущения по поводу отжима. Как-бы вторая сторона сама сделала для этого всё возможное.
Не хочу занудствовать, но теперь уже реально хочется понять.
Т.е. иными словами, неявно каждый метод, в дополнении к результату возвращает и некий код результата/ошибки, который вывел и назначил компилятор, а каждый вызов оборачивается анализом с передачей результата в продолжение если код Ок, либо переходом в соответствующий обработчик/немедленным возвращением этого же кода вверх по стеку?
Теперь я правильно понял?
Если речь об этом, то концептуально это одно и то же.
И проблема остаётся той же: мы не можем с уверенностью собрать все возможные типы ошибок так, чтобы ко всему прочему оно оставалось верным и во время исполнения. А значит в реальной жизни одним из самых частых кодов ошибки будет что-то вроде АНЭКСПЕКТЕД_РЕТУРН_КОД.
Ну и отдельный большой вопрос - почему нужно делать именно так, чем оно лучше?
Я понимаю, компилятор/ИДЕ/анализатор может находить некоторое подмножество возможных исключений и показывать это программисту/помогать обрабатывать.
В этом был бы смысл, если бы было возможно найти ВСЕ везможные исключения метода/функции. Но это невозможно, см. примеры Интерфейса/Абстрактного класса/Рантайма новой версии/Плагина.
Значит либо программисту всё равно придётся лезть в документацию сторонних библиотек/свой код - и даже это не поможет в случае обновления рантайма/сторонних библиотек на исполняющей машине, либо он будет оперировать неполной (==ложной) информацией. Что в итоге даёт нам одинаковый результат.
Вывод: "нинада". Перефразируя: лови, что можешь исправить и будь, что будет.
В моём понимании (и практике) ровно наоборот - условные "99% ошибок" внутри процессинга - это прерывающие исключения.
Всё, что не прерывает - это нормальный случай, он выбирается по условиям/анализу/валидации параметров-данных. Или тот самый Try*-pattern.
Да, бывают, опять же, внешние неконтролируемые зависимости с высокой частотой throw по любому поводу - приходится заменять или минимизировать ущерб.
Одноразовые (per API call/button press) проверки - вообще, как правило, не влияют на картину throughput, могут быть построены как угодно (до известной степени, конечно).
...если что - обработка миллиардов итераций per-API-call (в том числе с необходимостью обработки исключений на горячем пути) - специфика моих систем в последние долгие уже годы, так что "цену" разных мелочей я прочувствовал и понимаю. И как показывает моя практика - оптимизация нормального/expected случая "весит" куда больше, чем возможные исключения.
Не "без 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 и "утрамбовку" памяти для списка могут стать гораздо дешевле, чем для связного списка.
Не знаю как в вашем случае, а для какого-нибудь хайлоада/хай срупута я бы предпочел список (если быть до конца точным - структуру, основанную на массивах и не допускающую попадания в ЛОХ)
Есть опыт?
Я бы лично с удовольствием (и благодарностью) ознакомился, особенно под .НЕТ ниже седьмого.
Тут есть очень много "но".
Демонстрационный пример очень удачный, так как у нас простой фаст-трак/хэппи пас и несколько обработчиков исключительных ситуаций, куда мы выпадаем редко и передаём в них минимум параметров.
Теперь представим себе, что основная логика и она же - хэппи пас у нас реально объёмная. Она может быть простая, ветвлений минимум, фаст трак (проверки условий, после которых в 99% мы просто переходим на следующую инструкцию) - но объём метода в целом сотни строк, если не тысяча/чи.
Допустим, мы хотим сделать его менее врайт-онли и поделить на более короткие методы с ограниченными ответственностями. Ок, но теперь в каждый выделенный метод мы должны передавать весь необходимый контекст - что запросто может оказаться десятком параметров. А некоторые параметры у нас, к примеру, относительно объёмные структуры. Ок, передаём их по ссылке. Но, теперь у нас ещё и локальность данных размазалась по стеку.
Не уверен, что джит всё это заинлайнит и вернёт наш исходный фаст трак, даже если повесить на каждый метод агрессив инлайнинг.
Ну, как не уверен? Эмпирически знаю - не заинлайнит.
В итоге мы поделили наш фаст трак на несколько вызовов-возвратов, к каждому из которых добавляется копирование всех нужных параметров.
И ради чего? Читабельность? Это, конечно, хорошо, но не всегда стоит потерь производительности.
И это только первое из многих "но" :)
Я к чему? Без бенчмарков и профилирование на разных сценариях я лично никаких сложных и объёмных методов на горячем пути разделять не советовал бы.
...объединять, кстати, на моём опыте - менее рисковано.
Нет, массив всегда есть последовательный непрерывный блок памяти.
Оператор fixed просто запрещает GC перемещать блок памяти (на время своего действия).
А как ключевое слово - позволяет объявить инлайн-буфер заданной длины внутри unsafe-структуры.
Когда 10 байт лежат рядом в текущей кэш-линии - это одно, а когда их нужно прочитать непосредственно из DRAM - это совсем другое.
Ключевое слово здесь "не нужен доступ по индексу". На первый взгляд можно бы и согласиться - добавление, вставка и удаление будут быстрее.
Но есть нюансы.
Про кэш-линии уже упомянули.
Но моё любимое - GC.
Мусора такой список создаёт слишком много, чтобы быть быстрым [на более-менее долгом промежутке времени].
Я лично бенчмаркал все стандартные не-массив-бейсед коллекции (включая LinkedList, SortedList и SortedSet), и могу сказать, что для хайлоада они слишком много мусорят. Те, что tree-based - даже при енумерации.
Для себя нашел выход: если действительно не нужен доступ по индексу (и байнари сёрча и т.п.) - лучший вариант это связанный список, но не создающий ноду в куче на каждый элемент, а использующий struct ноды в предварительно выделенных массивах. Да, есть оверхед для учёта и переиспользования свободных/освободившихся слотов в массивах нод, оверхэд на выделение большего массива и копирование данных при добавлении сверх текущей ёмкости, но в целом это гораздо лучший вариант для списков, которые живут дольше одной-двух сборок мусора.
Было бы очень любопытно узнать больше деталей.
Тоже писал некоторым-образом-виртуальную-машину, правда входом для неё был не код, а граф датафлоу. Было бы интересно сравнить опыт.
Ну, вчера хозяева, завтра - никто.
Доля в собственности не отменяет необходимости порядочно себя вести.
Ну, как-бы кидок, не?
После попытки кидка любая реакция считается в пределах нормы.
Технично. Всё правильно сделали.
Любопытно, а если бы... европейцы не поделились никакими доступами - получилось бы добиться такого же результата?
И ещё, почему сразу не отрубили все внешние доступы? - я подозреваю, что подвох в любой момент был ожидаем, как решились дожидаться шагов с той стороны?
...не понимаю праведного возмущения по поводу отжима. Как-бы вторая сторона сама сделала для этого всё возможное.
Собственно, к чему я и вёл.
Пропустил слово: одним из самых частых ОБРАБАТЫВАЕМЫХ кодов ошибки будет что-то вроде АНЭКСПЕКТЕД_РЕТУРН_КОД.
Имелось ввиду "любой".
Не, любопытно было чем этот подход кажется лучше исключений. В сравнении с го [мне] понятно в чём смысл.
Не хочу занудствовать, но теперь уже реально хочется понять.
Т.е. иными словами, неявно каждый метод, в дополнении к результату возвращает и некий код результата/ошибки, который вывел и назначил компилятор, а каждый вызов оборачивается анализом с передачей результата в продолжение если код Ок, либо переходом в соответствующий обработчик/немедленным возвращением этого же кода вверх по стеку?
Теперь я правильно понял?
Если речь об этом, то концептуально это одно и то же.
И проблема остаётся той же: мы не можем с уверенностью собрать все возможные типы ошибок так, чтобы ко всему прочему оно оставалось верным и во время исполнения. А значит в реальной жизни одним из самых частых кодов ошибки будет что-то вроде АНЭКСПЕКТЕД_РЕТУРН_КОД.
Ну и отдельный большой вопрос - почему нужно делать именно так, чем оно лучше?
Ну, я про исключения - возвращение по стеку до ближайшего обработчика с сохранением какой-либо информации об исключительном собитии. А вы?
Я понимаю, компилятор/ИДЕ/анализатор может находить некоторое подмножество возможных исключений и показывать это программисту/помогать обрабатывать.
В этом был бы смысл, если бы было возможно найти ВСЕ везможные исключения метода/функции. Но это невозможно, см. примеры Интерфейса/Абстрактного класса/Рантайма новой версии/Плагина.
Значит либо программисту всё равно придётся лезть в документацию сторонних библиотек/свой код - и даже это не поможет в случае обновления рантайма/сторонних библиотек на исполняющей машине, либо он будет оперировать неполной (==ложной) информацией.
Что в итоге даёт нам одинаковый результат.
Вывод: "нинада". Перефразируя: лови, что можешь исправить и будь, что будет.
Примерно такой у меня ход мысли на этот счёт.