Считаете ли вы, что существует компромисс между написанием «хорошего» объектно-ориентированного кода и написанием очень быстрого кода с низкой задержкой? Например, избегать виртуальных функций в C ++ / накладных расходов на полиморфизм и т. Д. Переписывать код, который выглядит неприятно, но очень быстро и т. Д.?
Я работаю в области, которая немного больше сфокусирована на пропускной способности, чем на задержке, но это очень критично для производительности, и я бы сказал, «Сорта» .
Тем не менее, проблема заключается в том, что очень многие люди неправильно понимают свои представления о производительности. Новички часто все понимают неправильно, и вся их концептуальная модель «вычислительных затрат» требует доработки, и только алгоритмическая сложность - единственное, что они могут сделать правильно. Промежуточные получают много вещей неправильно. Эксперты ошибаются.
Измерение с помощью точных инструментов, которые могут обеспечить такие показатели, как пропуски в кеше и неправильные прогнозы в ветвях, - это то, что контролирует всех людей любого уровня знаний в этой области.
Измерение также указывает на то , что не следует оптимизировать . Эксперты часто тратят меньше времени на оптимизацию, чем новички, так как они оптимизируют истинно измеренные горячие точки и не пытаются оптимизировать дикие удары в темноте, основываясь на догадках о том, что может быть медленным (что в экстремальной форме может соблазнить человека на микрооптимизацию только о каждой другой строке в кодовой базе).
Проектирование для Производительности
Помимо этого, ключ к проектированию для повышения производительности исходит от части дизайна , как в дизайне интерфейса. Одна из проблем неопытности заключается в том, что, как правило, происходит ранний сдвиг в абсолютных показателях реализации, таких как стоимость косвенного вызова функции в некотором обобщенном контексте, как если бы это была стоимость (которая лучше понять в непосредственном смысле с точки зрения оптимизатора). точка зрения, а не разветвленная точка зрения) является причиной, чтобы избежать этого во всей кодовой базе.
Затраты являются относительными . В то время как есть косвенный вызов функции, например, все затраты являются относительными. Если вы платите эту стоимость один раз за вызов функции, которая проходит через миллионы элементов, то беспокойство об этой стоимости похоже на то, чтобы тратить часы на то, чтобы потратить деньги на покупку продукта стоимостью в миллиард долларов, только чтобы не покупать этот продукт, потому что была одна копейка слишком дорогой.
Грубый дизайн интерфейса
Интерфейс дизайн аспект производительности часто стремится раньше , чтобы подтолкнуть эти расходы до уровня крупнозернистого. Например, вместо того, чтобы платить затраты на абстракцию во время выполнения для одной частицы, мы могли бы поднять эти затраты до уровня системы / эмиттера частиц, эффективно преобразовав частицу в детали реализации и / или просто необработанные данные этой коллекции частиц.
Таким образом, объектно-ориентированный дизайн не обязательно должен быть несовместим с проектированием для повышения производительности (с задержкой или пропускной способностью), но в языке, который фокусируется на нем, могут возникнуть соблазны моделировать все более крошечные гранулированные объекты, и там последний оптимизатор не может Помогите. Он не может делать такие вещи, как объединение класса, представляющего одну точку, таким образом, чтобы получить эффективное представление SoA для шаблонов доступа к памяти программного обеспечения. Набор точек с дизайном интерфейса, смоделированным на уровне грубости, предоставляет такую возможность и позволяет итерацию к более и более оптимальным решениям по мере необходимости. Такой дизайн рассчитан на объемную память *.
* Обратите внимание на то, что внимание сосредоточено на памяти, а не на данных , так как работа в критически важных областях в течение длительного времени может привести к изменению вашего взгляда на типы данных и структуры данных и на то, как они подключаются к памяти. Бинарное дерево поиска больше не становится исключительно логарифмической сложностью в таких случаях, как, возможно, несопоставимые и непригодные к кешированию фрагменты памяти для узлов дерева, если этому не помогает фиксированный распределитель. Представление не отклоняет алгоритмическую сложность, но оно видит ее больше независимо от разметки памяти. Также начинают воспринимать итерации работы как об итерациях доступа к памяти. *
Многие критичные к производительности проекты могут быть действительно совместимы с концепцией высокоуровневых конструкций интерфейсов, которые легко понять и использовать людям. Разница заключается в том, что «высокий уровень» в этом контексте будет означать массовую агрегацию памяти, интерфейс, смоделированный для потенциально больших коллекций данных, и с реализацией под капотом, которая может быть довольно низкоуровневой. Визуальной аналогией может быть автомобиль, который действительно удобен, легок в управлении и управлении и очень безопасен при движении со скоростью звука, но если вы наденете капот, внутри останется мало огнедышащих демонов.
Более грубый дизайн также приводит к более простому способу обеспечения более эффективных шаблонов блокировки и использования параллелизма в коде (многопоточность - это исчерпывающий предмет, который я как бы пропущу здесь).
Пул памяти
Важным аспектом программирования с малой задержкой, вероятно, будет очень четкое управление памятью, чтобы улучшить местность ссылок, а также просто общую скорость выделения и освобождения памяти. Пользовательская память пула распределителя фактически повторяет тот же тип мышления дизайна, который мы описали. Это разработано для большого количества ; это разработано на грубом уровне. Он предварительно распределяет память в больших блоках и объединяет память, уже выделенную в небольших порциях.
Идея точно такая же: подтолкнуть дорогостоящие вещи (например, выделение фрагмента памяти по отношению к распределителю общего назначения) на более грубый и грубый уровень. Пул памяти предназначен для массового использования памяти .
Системы типов, разделяющие память
Одна из трудностей гранулированного объектно-ориентированного проектирования на любом языке состоит в том, что он часто хочет представить множество маленьких пользовательских типов и структур данных. Эти типы могут затем хотеть быть распределенными в маленьких маленьких кусочках, если они распределяются динамически.
Распространенным примером в C ++ может быть случай, когда требуется полиморфизм, когда естественным соблазном является выделение каждого экземпляра подкласса для распределителя памяти общего назначения.
Это приводит к тому, что разбиваются возможные смежные макеты памяти на мелкие кусочки, разбросанные по диапазону адресов, что приводит к большему количеству ошибок страниц и кешу.
Области, которые требуют детерминистского ответа с минимальной задержкой, без заиканий, - вероятно, это единственное место, где горячие точки не всегда сводятся к одному узкому месту, где крошечные неэффективности могут действительно «накапливаться» (то, что многие люди воображают что происходит неправильно с профилировщиком, чтобы держать их под контролем, но в полях, управляемых задержкой, на самом деле могут быть редкие случаи, когда накапливаются крошечные неэффективности). И многие из наиболее распространенных причин такого накопления могут быть следующие: чрезмерное распределение маленьких кусочков памяти повсюду.
В таких языках, как Java, может быть полезно использовать больше массивов простых старых типов данных, когда это возможно, для узких областей (областей, обрабатываемых в виде замкнутых циклов), таких как массив int
(но все еще за громоздким высокоуровневым интерфейсом) вместо, скажем, , ArrayList
из определенных пользователем Integer
объектов. Это позволяет избежать сегрегации памяти, которая обычно сопровождает последнее. В C ++ нам не нужно так сильно ухудшать структуру, если наши шаблоны распределения памяти эффективны, поскольку пользовательские типы могут размещаться там непрерывно и даже в контексте универсального контейнера.
Слияние памяти снова вместе
Решение здесь состоит в том, чтобы найти пользовательский распределитель для однородных типов данных и, возможно, даже для однородных типов данных. Когда крошечные типы данных и структуры данных сглаживаются в битах и байтах в памяти, они приобретают однородный характер (хотя и с некоторыми различными требованиями к выравниванию). Когда мы не смотрим на них с точки зрения памяти, система типов языков программирования «хочет» разделить / разделить потенциально смежные области памяти на маленькие разбросанные кусочки.
Стек использует этот ориентированный на память фокус, чтобы избежать этого и потенциально хранить в нем любую возможную смешанную комбинацию экземпляров определенного пользователем типа. Использование стека больше - хорошая идея, когда это возможно, так как его верхняя часть почти всегда находится в строке кэша, но мы также можем спроектировать распределители памяти, которые имитируют некоторые из этих характеристик без шаблона LIFO, объединяя память между разнородными типами данных в непрерывные куски даже для более сложных моделей выделения памяти и освобождения.
Современное оборудование разработано таким образом, чтобы быть на пике при обработке смежных блоков памяти (например, многократный доступ к одной и той же строке кэша, к одной и той же странице). Ключевое слово там - смежность, поскольку это выгодно, только если есть окружающие данные, представляющие интерес. Таким образом, ключом (но также и трудностью) к производительности является объединение отдельных фрагментов памяти снова вместе в непрерывные блоки, к которым осуществляется доступ полностью (все относящиеся к ним данные имеют отношение) до выселения. Самым большим препятствием здесь может быть система богатых типов, в особенности определяемых пользователем типов в языках программирования, но мы всегда можем обойти и решить проблему с помощью специального распределителя и / или более объемных конструкций, когда это необходимо.
уродливый
«Гадкий» сложно сказать. Это субъективная метрика, и тот, кто работает в очень критичной для производительности области, начнет менять свое представление о «красоте» на концепцию, которая намного более ориентирована на данные и фокусируется на интерфейсах, которые обрабатывают вещи в большом объеме.
опасно
«Опасный» может быть проще. В целом, производительность стремится достичь низкоуровневого кода. Например, реализация распределителя памяти невозможна без достижения типов данных и работы на опасном уровне необработанных битов и байтов. В результате это может помочь сосредоточиться на тщательной процедуре тестирования в этих критичных к производительности подсистемах, масштабируя тщательность тестирования с уровнем примененной оптимизации.
красота
Тем не менее, все это будет на уровне детализации реализации. Как в ветеране масштабного, так и в критическом отношении к производительности «красота» имеет тенденцию смещаться в сторону дизайна интерфейса, а не деталей реализации. Это становится экспоненциально более высоким приоритетом, чтобы искать «красивые», пригодные для использования, безопасные, эффективные интерфейсы, а не реализации из-за связывания и каскадных разрывов, которые могут произойти перед лицом изменения дизайна интерфейса. Реализации могут быть заменены в любое время. Обычно мы выполняем итерацию к производительности по мере необходимости и как показывают измерения. Ключом к дизайну интерфейса является моделирование на достаточно грубом уровне, чтобы оставить место для таких итераций, не нарушая всю систему.
На самом деле, я хотел бы предположить, что ветеранов, сосредоточенных на разработке, критически важной для производительности, часто будут стремиться сделать основной упор на безопасность, тестирование, ремонтопригодность, просто ученик SE в целом, так как крупномасштабная кодовая база, которая имеет ряд производительности -критические подсистемы (системы частиц, алгоритмы обработки изображений, обработка видео, звуковая обратная связь, трассировщики лучей, механизмы построения ячеек и т. д.) должны уделять пристальное внимание разработке программного обеспечения, чтобы избежать утопления в кошмаре обслуживания. Не случайно, что самые удивительно эффективные продукты также могут иметь наименьшее количество ошибок.
TL; DR
Как бы то ни было, это мой взгляд на эту тему, начиная от приоритетов в действительно критических с точки зрения производительности областях, что может уменьшить задержку и вызвать крошечную неэффективность, и что на самом деле составляет «красоту» (если смотреть на вещи наиболее продуктивно).