Язык арифметических выражений придуман не очень хороший, отсюда "прагматические недостатки" 2-4.
Синтаксисы let и where как-то дублируют друг друга.
Кажется, из-за вложенности классов допустимо что-то вроде Result.Some.Some.Error("...") (не уверен). Это было бы странно для DSL.
Без приоритета операций совсем никуда — нехорошо, если выражение 2 + 1 * 3 внезапно равно 9.
Нагляднее было бы представлять выражения в виде деревьев, тогда могло бы появиться "описание грамматики" (в виде case-классов, как и у вас), приоритет операций стал бы не нужен, и наверно можно было бы ввести какую-то типизацию.
Пример:
val expr = If(
Less(
Var("x"),
Const(3)),
Mul(Var("y"),Const(4)),
Mul(
Const(5),
Add(
Const(1),
Const(2)))) where (
"x" eq 43,
"y" eq 3)
Здесь синтаксический сахар языка Scala использован не в полной мере, т.к. я не знаю, что из сахара есть в Kotlin. Это — минималистичный по использованию сахара вариант. Можно писать и короче, типа Mul (var"x", var"y", 5, 6)
x86-64 процессор имеет 16 регистров для целых чисел и 16 YMM регистров для чисел с плавающей точкой.
Читал, что в суперскалярных процессорах (по крайней мере, начиная с Pentium) число физических регистров существенно превышает число архитектурных (физических несколько сотен, архитектурных 16). Почему ухудшение производительности у вас проявлялось при 20 аккумуляторах? Можете нащупать границу более точно? Возможно, производительность ограничивалась чем-то другим?
Не сразу заметил, что под временем доступа O(√N) имеется в виду среднее время доступа к объёму памяти N (именно это, а не что-то другое, замеряет автор своими тестами), а не худшее время одиночного чтения.
В рамках этой статьи мы O(f(N)) будет означать, что f(N) — это верхняя граница (худший случай) по времени, которое необходимо для получения доступа к N байтов памяти (или, соответственно, N одинаковых по размеру элементов).
Так — да, почти сходится. Вместо O(N) из-за кэша получается что-то вроде O(√N), когда есть повторные обращения. Но не вместо O(1), не вместо единичного чтения! И замеряется не худший случай, а средний.
Аналогия с хождением по библиотеке странная. Когда контроллер памяти выставляет 32 бита адреса, он сразу (через фиксированное число тактов) получает доступ к любому из 2^32 байт информации. Если 64 бита — к любому из 2^64 байт. Никто никуда не ходит! Латентность не растёт пропорционально корню количества данных.
Аналогия с чёрной дырой ещё страннее. При современных (невысоких) плотностях и объёмах наверняка можно хранить информацию на узлах в трёхмерной структуре в количестве, пропорциональном объёму структуры, а не площади.
Очень спорная статья, с неверной методикой замеров и странными размышлениями. Неудивительно, что столько людей посчитали её юмором.
Обращение к одному и тому же участку — это не произвольный доступ. Даже если это не один блок памяти, а множество участков, расположенных не подряд, как в связном списке.
В статье почему-то говорится о произвольном доступе, хотя тесты проводятся много итераций для одного и того же списка (я посмотрел код автора на гитхабе). Естественно, список кэшируется (полностью или нет). И это влияет на результаты тестов. Если для каждой итерации брать разные списки (расположенные в некэшированных участках памяти) — полагаю, результаты будут другими.
А кэш очищался перед каждым прогоном теста?
Если нет — в первом прогоне (или даже в генерации данных) было обращение к памяти, а во втором и следующих прогонах измерялась скорость повторного доступа к кэшу. То есть, не было случайного паттерна доступа и проявлялось ускорение за счёт кэширования (пологие участки на графиках).
Имхо, выигрывает. В funcparserlib нет лишних управляющих структур (for/yield), которые есть в коде из статьи. Это значит, что без этих структур можно обойтись, и следовательно они не нужны.
И что-то мне подсказывает, что по 2 yield'а в каждой функции — не Python-way (это не выглядит просто).
Комбинаторные парсеры (пример выше) выглядят гораздо "изящнее", чем for'ы вместо последовательностей термов. (Если эти for'ы убрать, то и получатся комбинаторные парсеры. А так — "код в стиле барокко").
Рекурсивный спуск выглядит проще (но объёмнее).
И ещё есть всякие генераторы парсеров, которые должны работать быстрее.
Статья хорошо показывает, что парсер с нуля написать несложно, но я бы не рекомендовал использовать конкретно этот подход. Он избыточен и неочевиден.
Весьма странно видеть MPI и OpenMP как замену SIMD. Они не занимаются векторизацией.
OpenMP — это распараллеливание программы между потоками, и оно помогает задействовать совершенно другие возможности процессоров (многоядерность), чем SIMD (широкие АЛУ).
MPI (если речь идёт о Message Passing Interface) — это вообще про обмен сообщениями между процессами в кластере. Я бы не хотел видеть такое в стандарте языка общего назначения.
К слову, OpenMP-подход (т.е. расширение компилятора для поддержки потоков) сейчас слабо распространён в разных языках, хотя удобен для пользователя. Возможно, не очень хорошо/не очень удобно модифицировать компилятор для поддержки многопоточности, а написать библиотеку намного проще.
Чаще предпочтение отдаётся подходу с параллельным коллекциями. Но, как правило, это тоже не векторизация.
В своё время я пытался переупаковать HttpComponents с помощью maven shade и gradle на этапе сборки (не вышло, где-то использовалась рефлексия), а потом я увидел, что авторы HttpComponents (не от хорошей жизни) предоставляют отдельную сборку под android с другими названиями классов. Но этот вариант никак не подходил (я писал библиотеку, зависящую от HttpComponents), и пришлось отказаться от HttpComponents.
Это печально. Надо бы предусмотреть пункт в лицензии библиотек на случай подобного использования.
Google довольно часто руководствуется принципом "здесь и сейчас", игнорируя правила, и через некоторое время пользователи остаются наедине с негибкими решениями.
Навскидку:
Описанная ситуация с нестабильной версией HttpComponents (включить нестабильную библиотеку в ОС, и тем самым запретить её обновления без костылей? серьёзно?)
Ограничение 64К методов в .dex (конечно, этого хватит всем).
Отсутствие generic-ов в go (сойдёт и так, пока вам не понадобятся самописные коллекции).
Отказ от использования исключений в языках, в которых они есть (в их библиотеках на С++ и по инерции на Java).
Нестандартное для Java соглашение об именовании полей (префикс m).
В принципе, этого достаточно, чтобы подходить с осторожностью к использованию их библиотек/технологий.
Размер потребляемых ресурсов: памяти, процессорного времени, используемых ядер, необходимость или отсутствие доп. оборудования.
Если вы не смогли описать в интерфейсе эти параметры, это проблема конкретного случая. Опишите их в документации, если число ядер или время обработки — действительно часть интерфейса. Из невозможности полностью описать интерфейс средствами языка не следует вывод, что LSP неверен.
В пределе Вы не можете изменить свой код сразу после того как поднимите руки над клавиатурой.
Если вы лезете с клавиатурой прямо в продакшен, то да.
А на практике код можно менять как угодно, если вы в состоянии переписать все участки программы/инфраструктуры, на которые влияет изменение, так, чтобы клиенты не пострадали. OCP, если его применять, только минимизирует число изменений других частей.
В статье очень много максимализма, как уже говорилось выше. Эти принципы намного гибче, чем вам кажется. И SOLID действительно помогает поддерживать и развивать огромные системы.
Проверку типов реализовать несложно:
Язык арифметических выражений придуман не очень хороший, отсюда "прагматические недостатки" 2-4.
Синтаксисы let и where как-то дублируют друг друга.
Кажется, из-за вложенности классов допустимо что-то вроде Result.Some.Some.Error("...") (не уверен). Это было бы странно для DSL.
Без приоритета операций совсем никуда — нехорошо, если выражение 2 + 1 * 3 внезапно равно 9.
Нагляднее было бы представлять выражения в виде деревьев, тогда могло бы появиться "описание грамматики" (в виде case-классов, как и у вас), приоритет операций стал бы не нужен, и наверно можно было бы ввести какую-то типизацию.
Здесь синтаксический сахар языка Scala использован не в полной мере, т.к. я не знаю, что из сахара есть в Kotlin. Это — минималистичный по использованию сахара вариант. Можно писать и короче, типа Mul (var"x", var"y", 5, 6)
a[i] и *(a+i) эквивалентны, и поэтому компилируются абсолютно одинаково.
Спасибо за статью.
Читал, что в суперскалярных процессорах (по крайней мере, начиная с Pentium) число физических регистров существенно превышает число архитектурных (физических несколько сотен, архитектурных 16). Почему ухудшение производительности у вас проявлялось при 20 аккумуляторах? Можете нащупать границу более точно? Возможно, производительность ограничивалась чем-то другим?
А почему не на макросах? case классы — это же элементарно и совершенно очевидно.
Некоторые даже на С++ шаблонах дифференцирование делают.
Не сразу заметил, что под временем доступа O(√N) имеется в виду среднее время доступа к объёму памяти N (именно это, а не что-то другое, замеряет автор своими тестами), а не худшее время одиночного чтения.
Так — да, почти сходится. Вместо O(N) из-за кэша получается что-то вроде O(√N), когда есть повторные обращения. Но не вместо O(1), не вместо единичного чтения! И замеряется не худший случай, а средний.
Аналогия с хождением по библиотеке странная. Когда контроллер памяти выставляет 32 бита адреса, он сразу (через фиксированное число тактов) получает доступ к любому из 2^32 байт информации. Если 64 бита — к любому из 2^64 байт. Никто никуда не ходит! Латентность не растёт пропорционально корню количества данных.
Аналогия с чёрной дырой ещё страннее. При современных (невысоких) плотностях и объёмах наверняка можно хранить информацию на узлах в трёхмерной структуре в количестве, пропорциональном объёму структуры, а не площади.
Очень спорная статья, с неверной методикой замеров и странными размышлениями. Неудивительно, что столько людей посчитали её юмором.
Обращение к одному и тому же участку — это не произвольный доступ. Даже если это не один блок памяти, а множество участков, расположенных не подряд, как в связном списке.
В статье почему-то говорится о произвольном доступе, хотя тесты проводятся много итераций для одного и того же списка (я посмотрел код автора на гитхабе). Естественно, список кэшируется (полностью или нет). И это влияет на результаты тестов. Если для каждой итерации брать разные списки (расположенные в некэшированных участках памяти) — полагаю, результаты будут другими.
А кэш очищался перед каждым прогоном теста?
Если нет — в первом прогоне (или даже в генерации данных) было обращение к памяти, а во втором и следующих прогонах измерялась скорость повторного доступа к кэшу. То есть, не было случайного паттерна доступа и проявлялось ускорение за счёт кэширования (пологие участки на графиках).
Да, но do-синтаксис менее многословен и семантически больше подходит.
Имхо, выигрывает. В funcparserlib нет лишних управляющих структур (for/yield), которые есть в коде из статьи. Это значит, что без этих структур можно обойтись, и следовательно они не нужны.
И что-то мне подсказывает, что по 2 yield'а в каждой функции — не Python-way (это не выглядит просто).
Комбинаторные парсеры (пример выше) выглядят гораздо "изящнее", чем for'ы вместо последовательностей термов. (Если эти for'ы убрать, то и получатся комбинаторные парсеры. А так — "код в стиле барокко").
Рекурсивный спуск выглядит проще (но объёмнее).
И ещё есть всякие генераторы парсеров, которые должны работать быстрее.
Статья хорошо показывает, что парсер с нуля написать несложно, но я бы не рекомендовал использовать конкретно этот подход. Он избыточен и неочевиден.
И почему Python 2 ?
Весьма странно видеть MPI и OpenMP как замену SIMD. Они не занимаются векторизацией.
OpenMP — это распараллеливание программы между потоками, и оно помогает задействовать совершенно другие возможности процессоров (многоядерность), чем SIMD (широкие АЛУ).
MPI (если речь идёт о Message Passing Interface) — это вообще про обмен сообщениями между процессами в кластере. Я бы не хотел видеть такое в стандарте языка общего назначения.
К слову, OpenMP-подход (т.е. расширение компилятора для поддержки потоков) сейчас слабо распространён в разных языках, хотя удобен для пользователя. Возможно, не очень хорошо/не очень удобно модифицировать компилятор для поддержки многопоточности, а написать библиотеку намного проще.
Чаще предпочтение отдаётся подходу с параллельным коллекциями. Но, как правило, это тоже не векторизация.
Если фабрика может вернуть nullptr, вызов метода без проверки на nullptr не является "адекватным".
Класслоадер по-хорошему сначала должен искать стандартные классы (которые уже есть у его родителя), а потом — свои.
В своё время я пытался переупаковать HttpComponents с помощью maven shade и gradle на этапе сборки (не вышло, где-то использовалась рефлексия), а потом я увидел, что авторы HttpComponents (не от хорошей жизни) предоставляют отдельную сборку под android с другими названиями классов. Но этот вариант никак не подходил (я писал библиотеку, зависящую от HttpComponents), и пришлось отказаться от HttpComponents.
Это печально. Надо бы предусмотреть пункт в лицензии библиотек на случай подобного использования.
Google довольно часто руководствуется принципом "здесь и сейчас", игнорируя правила, и через некоторое время пользователи остаются наедине с негибкими решениями.
В принципе, этого достаточно, чтобы подходить с осторожностью к использованию их библиотек/технологий.
А почему вы считаете трупами большие, но работающие, развивающиеся и приносящие деньги системы?
Если вы не смогли описать в интерфейсе эти параметры, это проблема конкретного случая. Опишите их в документации, если число ядер или время обработки — действительно часть интерфейса. Из невозможности полностью описать интерфейс средствами языка не следует вывод, что LSP неверен.
Если вы лезете с клавиатурой прямо в продакшен, то да.
А на практике код можно менять как угодно, если вы в состоянии переписать все участки программы/инфраструктуры, на которые влияет изменение, так, чтобы клиенты не пострадали. OCP, если его применять, только минимизирует число изменений других частей.
В статье очень много максимализма, как уже говорилось выше. Эти принципы намного гибче, чем вам кажется. И SOLID действительно помогает поддерживать и развивать огромные системы.