(Это в основном предназначено для тех, кто обладает специфическими знаниями о системах с низкой задержкой, чтобы люди просто не отвечали безосновательными мнениями).
Считаете ли вы, что существует компромисс между написанием «хорошего» объектно-ориентированного кода и написанием очень быстрого кода с низкой задержкой? Например, избегать виртуальных функций в C ++ / накладных расходов на полиморфизм и т. Д. Переписывать код, который выглядит неприятно, но очень быстро и т. Д.?
Это само собой разумеющееся - кого это волнует, если оно выглядит некрасиво (если его можно обслуживать) - если вам нужна скорость, вам нужна скорость?
Мне было бы интересно услышать от людей, которые работали в таких областях.
Ответы:
Да.
Вот почему существует фраза «преждевременная оптимизация». Он существует для того, чтобы заставить разработчиков измерять свою производительность и оптимизировать только тот код, который будет влиять на производительность, и в то же время разумно разрабатывать архитектуру своего приложения с самого начала, чтобы он не падал при большой нагрузке.
Таким образом, в максимально возможной степени вы сохраняете свой красивый, хорошо спроектированный, объектно-ориентированный код и оптимизируете с помощью уродливого кода только те небольшие порции, которые имеют значение.
источник
Да, пример, который я привожу, не C ++ против Java, но сборка против COBOL, поскольку это то, что я знаю.
Оба языка очень быстрые, но даже в языке COBOL при компиляции имеется гораздо больше инструкций, которые помещаются в набор инструкций, которые не обязательно должны быть там, по сравнению с написанием этих инструкций самостоятельно в ассемблере.
Эта же идея может быть применена непосредственно к вашему вопросу о написании «некрасивого кода» по сравнению с использованием наследования / полиморфизма в C ++. Я считаю, что необходимо писать некрасиво выглядящий код, если конечному пользователю нужны временные интервалы транзакции в доли секунды, то наша задача, как программистов, дать им это независимо от того, как это происходит.
При этом свободное использование комментариев значительно повышает функциональность и удобство обслуживания программиста, независимо от того, насколько уродлив код.
источник
Да, компромисс существует. Под этим я подразумеваю, что код, который быстрее и уродливее, лучше не нужен - количественные выгоды от «быстрого кода» необходимо сопоставлять со сложностью обслуживания изменений кода, необходимых для достижения этой скорости.
Компромисс происходит от стоимости бизнеса. Для более сложного кода требуются более опытные программисты (и программисты с более сфокусированным набором навыков, например, обладающие архитектурой процессора и знаниями в области проектирования), требуется больше времени для чтения и понимания кода, а также для исправления ошибок. Бизнес-затраты на разработку и поддержку такого кода могут быть в 10–100 раз выше, чем при написании обычного кода.
Такая стоимость обслуживания оправдана в некоторых отраслях , в которых клиенты готовы платить очень высокую надбавку за очень быстрое программное обеспечение.
Некоторые оптимизации скорости обеспечивают более высокую рентабельность инвестиций, чем другие. А именно, некоторые методы оптимизации могут применяться с меньшим влиянием на удобство сопровождения кода (сохранение структуры более высокого уровня и читаемость более низкого уровня) по сравнению с обычно написанным кодом.
Таким образом, владелец бизнеса должен:
Эти компромиссы очень специфичны для обстоятельств.
Они не могут быть оптимально решены без участия менеджеров и владельцев продуктов.
Они очень специфичны для платформ. Например, настольные и мобильные процессоры имеют разные соображения. Серверные и клиентские приложения также имеют разные соображения.
Да, как правило, более быстрый код выглядит не так, как обычно написанный код. Любой другой код займет больше времени для чтения. Означает ли это безобразие в глазах смотрящего.
Методы, с которыми я сталкиваюсь, это: (не пытаясь претендовать на какой-либо уровень знаний) оптимизация коротких векторов (SIMD), детализированный параллелизм задач, предварительное выделение памяти и повторное использование объектов.
SIMD обычно оказывает серьезное влияние на низкоуровневую читаемость, хотя обычно для этого не требуются структурные изменения более высокого уровня (при условии, что API разработан с учетом предотвращения узких мест).
Некоторые алгоритмы могут быть легко преобразованы в SIMD (смущающая векторизация). Некоторые алгоритмы требуют большего количества перестановок вычислений, чтобы использовать SIMD. В крайних случаях, таких как параллелизм SIMD с волновым фронтом, необходимо использовать совершенно новые алгоритмы (и патентоспособные реализации), чтобы воспользоваться ими.
Детализированное распараллеливание задач требует реорганизации алгоритмов в графы потоков данных и многократного применения функциональной (вычислительной) декомпозиции к алгоритму до тех пор, пока не будет получено дополнительное преимущество. Разложенные этапы обычно связаны в стиле продолжения, концепции, заимствованной из функционального программирования.
С помощью функциональной (вычислительной) декомпозиции алгоритмы, которые могли бы быть нормально написаны в линейной и концептуально четкой последовательности (строки кода, которые выполняются в том же порядке, в котором они написаны), должны быть разбиты на фрагменты и распределены по нескольким функциям. или классы. (См. «Объективация алгоритма» ниже.) Это изменение сильно помешает коллегам-программистам, которые не знакомы с процессом разработки декомпозиции, который привел к созданию такого кода.
Чтобы сделать такой код поддерживаемым, авторы такого кода должны написать тщательно разработанную документацию алгоритма - далеко за пределами вида комментирования кода или диаграмм UML, сделанных для нормально написанного кода. Это похоже на то, как исследователи пишут свои научные статьи.
Нет, быстрый код не должен противоречить объектно-ориентированности.
Иными словами, можно реализовать очень быстрое программное обеспечение, которое все еще является объектно-ориентированным. Тем не менее, ближе к нижнему концу этой реализации (на уровне «гайки и болты», где происходит большинство вычислений), проектирование объекта может значительно отличаться от проектов, полученных из объектно-ориентированного проектирования (OOD). Проект нижнего уровня направлен на объективизацию алгоритма.
Некоторые преимущества объектно-ориентированного программирования (ООП), такие как инкапсуляция, полиморфизм и композиция, все еще могут быть получены из низкоуровневого алгоритмического объективирования. Это основное обоснование использования ООП на этом уровне.
Большинство преимуществ объектно-ориентированного проектирования (OOD) потеряны. Самое главное, что в дизайне низкого уровня нет интуитивности. Другой программист не может научиться работать с кодом более низкого уровня, не понимая сначала, как алгоритм был преобразован и разложен, и это понимание невозможно получить из полученного кода.
источник
Да, иногда код должен быть «уродливым», чтобы заставить его работать в нужное время, хотя весь код не должен быть уродливым. Производительность должна быть проверена и профилирована прежде, чтобы найти фрагменты кода, которые должны быть «некрасивыми», и эти разделы должны быть отмечены комментарием, чтобы будущие разработчики знали, что целенаправленно уродливо, а что просто лень. Если кто-то пишет много плохо разработанного кода, требующего повышения производительности, заставьте его доказать это.
Скорость так же важна, как и любое другое требование программы, так как неправильная коррекция управляемой ракеты эквивалентна правильной коррекции после удара. Поддерживаемость всегда является второстепенной задачей для рабочего кода.
источник
Некоторые из исследований, которые я видел, показывают, что очистка легко читаемого кода часто происходит быстрее, чем более сложный и трудно читаемый код. Частично это связано с тем, как оптимизаторы разработаны. Они, как правило, гораздо лучше оптимизируют переменную в регистре, чем делают то же самое с промежуточным результатом вычисления. Длинные последовательности назначений с использованием одного оператора, приводящего к конечному результату, могут быть оптимизированы лучше, чем длинное сложное уравнение. Новые оптимизаторы, возможно, уменьшили разницу между чистым и сложным кодом, но я сомневаюсь, что они устранили его.
Другие оптимизации, такие как развертывание цикла, могут быть добавлены чистым способом, когда это необходимо.
Любая оптимизация, добавленная для улучшения производительности, должна сопровождаться соответствующим комментарием. Это должно включать утверждение, что оно было добавлено в качестве оптимизации, предпочтительно с показателями производительности до и после.
Я обнаружил, что правило 80/20 применяется к коду, который я оптимизировал. Как правило, я не оптимизирую ничего, что не занимает как минимум 80% времени. Затем я стремлюсь (и обычно достигаю) 10-кратное увеличение производительности. Это повышает производительность примерно в 4 раза. Большинство оптимизаций, которые я реализовал, не сделали код значительно менее «красивым». Ваш пробег может варьироваться.
источник
Если под этим уродливым подразумевается трудность для чтения / понимания на уровне, на котором другие разработчики будут повторно использовать его или будут нуждаться в его понимании, то я бы сказал, что элегантный, легко читаемый код почти всегда в конечном итоге принесет вам повышение производительности в долгосрочной перспективе в приложении, которое вы должны поддерживать.
В противном случае иногда выигрыша в производительности достаточно, чтобы сделать его уродливым в красивой коробке с потрясающим интерфейсом, но, по моему опыту, это довольно редкая дилемма.
Подумайте об основных уклонениях от работы, как вы идете. Сохраните тайные уловки для случаев, когда проблема производительности фактически представляет себя. И если вам действительно нужно написать что-то, что кто-то может понять только благодаря знакомству с конкретной оптимизацией, сделайте все, что вы можете, чтобы хотя бы сделать уродливое легко понятным с точки зрения повторного использования вашей кодовой точки зрения. Код, который работает с жалостью, редко когда-либо делает это, потому что разработчики слишком много думали о том, что следующий человек унаследует, но если частые изменения являются единственной константой приложения (большинство веб-приложений в моем опыте), жесткий / негибкий код, который Трудно изменить, практически попрошайничать о панических беспорядках, чтобы они начали появляться по всей вашей кодовой базе. Чистота и стройность лучше для производительности в долгосрочной перспективе.
источник
Сложный и уродливый не одно и то же. Код, который имеет много особых случаев, оптимизирован для извлечения всех до последней капли производительности, и который поначалу выглядит как клубок связей и зависимостей, на самом деле может быть очень тщательно спроектирован и довольно красив, как только вы его поймете. Действительно, если производительность (измеряется ли она с точки зрения задержки или чего-то еще) достаточно важна для оправдания очень сложного кода, тогда код должен быть хорошо спроектирован. Если это не так, то вы не можете быть уверены, что вся эта сложность действительно лучше простого решения.
Для меня уродливый код - это небрежный, плохо продуманный и / или излишне сложный код . Я не думаю, что вы захотите какие-либо из этих функций в коде, который должен работать.
источник
Я работаю в области, которая немного больше сфокусирована на пропускной способности, чем на задержке, но это очень критично для производительности, и я бы сказал, «Сорта» .
Тем не менее, проблема заключается в том, что очень многие люди неправильно понимают свои представления о производительности. Новички часто все понимают неправильно, и вся их концептуальная модель «вычислительных затрат» требует доработки, и только алгоритмическая сложность - единственное, что они могут сделать правильно. Промежуточные получают много вещей неправильно. Эксперты ошибаются.
Измерение с помощью точных инструментов, которые могут обеспечить такие показатели, как пропуски в кеше и неправильные прогнозы в ветвях, - это то, что контролирует всех людей любого уровня знаний в этой области.
Измерение также указывает на то , что не следует оптимизировать . Эксперты часто тратят меньше времени на оптимизацию, чем новички, так как они оптимизируют истинно измеренные горячие точки и не пытаются оптимизировать дикие удары в темноте, основываясь на догадках о том, что может быть медленным (что в экстремальной форме может соблазнить человека на микрооптимизацию только о каждой другой строке в кодовой базе).
Проектирование для Производительности
Помимо этого, ключ к проектированию для повышения производительности исходит от части дизайна , как в дизайне интерфейса. Одна из проблем неопытности заключается в том, что, как правило, происходит ранний сдвиг в абсолютных показателях реализации, таких как стоимость косвенного вызова функции в некотором обобщенном контексте, как если бы это была стоимость (которая лучше понять в непосредственном смысле с точки зрения оптимизатора). точка зрения, а не разветвленная точка зрения) является причиной, чтобы избежать этого во всей кодовой базе.
Затраты являются относительными . В то время как есть косвенный вызов функции, например, все затраты являются относительными. Если вы платите эту стоимость один раз за вызов функции, которая проходит через миллионы элементов, то беспокойство об этой стоимости похоже на то, чтобы тратить часы на то, чтобы потратить деньги на покупку продукта стоимостью в миллиард долларов, только чтобы не покупать этот продукт, потому что была одна копейка слишком дорогой.
Грубый дизайн интерфейса
Интерфейс дизайн аспект производительности часто стремится раньше , чтобы подтолкнуть эти расходы до уровня крупнозернистого. Например, вместо того, чтобы платить затраты на абстракцию во время выполнения для одной частицы, мы могли бы поднять эти затраты до уровня системы / эмиттера частиц, эффективно преобразовав частицу в детали реализации и / или просто необработанные данные этой коллекции частиц.
Таким образом, объектно-ориентированный дизайн не обязательно должен быть несовместим с проектированием для повышения производительности (с задержкой или пропускной способностью), но в языке, который фокусируется на нем, могут возникнуть соблазны моделировать все более крошечные гранулированные объекты, и там последний оптимизатор не может Помогите. Он не может делать такие вещи, как объединение класса, представляющего одну точку, таким образом, чтобы получить эффективное представление SoA для шаблонов доступа к памяти программного обеспечения. Набор точек с дизайном интерфейса, смоделированным на уровне грубости, предоставляет такую возможность и позволяет итерацию к более и более оптимальным решениям по мере необходимости. Такой дизайн рассчитан на объемную память *.
Многие критичные к производительности проекты могут быть действительно совместимы с концепцией высокоуровневых конструкций интерфейсов, которые легко понять и использовать людям. Разница заключается в том, что «высокий уровень» в этом контексте будет означать массовую агрегацию памяти, интерфейс, смоделированный для потенциально больших коллекций данных, и с реализацией под капотом, которая может быть довольно низкоуровневой. Визуальной аналогией может быть автомобиль, который действительно удобен, легок в управлении и управлении и очень безопасен при движении со скоростью звука, но если вы наденете капот, внутри останется мало огнедышащих демонов.
Более грубый дизайн также приводит к более простому способу обеспечения более эффективных шаблонов блокировки и использования параллелизма в коде (многопоточность - это исчерпывающий предмет, который я как бы пропущу здесь).
Пул памяти
Важным аспектом программирования с малой задержкой, вероятно, будет очень четкое управление памятью, чтобы улучшить местность ссылок, а также просто общую скорость выделения и освобождения памяти. Пользовательская память пула распределителя фактически повторяет тот же тип мышления дизайна, который мы описали. Это разработано для большого количества ; это разработано на грубом уровне. Он предварительно распределяет память в больших блоках и объединяет память, уже выделенную в небольших порциях.
Идея точно такая же: подтолкнуть дорогостоящие вещи (например, выделение фрагмента памяти по отношению к распределителю общего назначения) на более грубый и грубый уровень. Пул памяти предназначен для массового использования памяти .
Системы типов, разделяющие память
Одна из трудностей гранулированного объектно-ориентированного проектирования на любом языке состоит в том, что он часто хочет представить множество маленьких пользовательских типов и структур данных. Эти типы могут затем хотеть быть распределенными в маленьких маленьких кусочках, если они распределяются динамически.
Распространенным примером в C ++ может быть случай, когда требуется полиморфизм, когда естественным соблазном является выделение каждого экземпляра подкласса для распределителя памяти общего назначения.
Это приводит к тому, что разбиваются возможные смежные макеты памяти на мелкие кусочки, разбросанные по диапазону адресов, что приводит к большему количеству ошибок страниц и кешу.
Области, которые требуют детерминистского ответа с минимальной задержкой, без заиканий, - вероятно, это единственное место, где горячие точки не всегда сводятся к одному узкому месту, где крошечные неэффективности могут действительно «накапливаться» (то, что многие люди воображают что происходит неправильно с профилировщиком, чтобы держать их под контролем, но в полях, управляемых задержкой, на самом деле могут быть редкие случаи, когда накапливаются крошечные неэффективности). И многие из наиболее распространенных причин такого накопления могут быть следующие: чрезмерное распределение маленьких кусочков памяти повсюду.
В таких языках, как Java, может быть полезно использовать больше массивов простых старых типов данных, когда это возможно, для узких областей (областей, обрабатываемых в виде замкнутых циклов), таких как массив
int
(но все еще за громоздким высокоуровневым интерфейсом) вместо, скажем, ,ArrayList
из определенных пользователемInteger
объектов. Это позволяет избежать сегрегации памяти, которая обычно сопровождает последнее. В C ++ нам не нужно так сильно ухудшать структуру, если наши шаблоны распределения памяти эффективны, поскольку пользовательские типы могут размещаться там непрерывно и даже в контексте универсального контейнера.Слияние памяти снова вместе
Решение здесь состоит в том, чтобы найти пользовательский распределитель для однородных типов данных и, возможно, даже для однородных типов данных. Когда крошечные типы данных и структуры данных сглаживаются в битах и байтах в памяти, они приобретают однородный характер (хотя и с некоторыми различными требованиями к выравниванию). Когда мы не смотрим на них с точки зрения памяти, система типов языков программирования «хочет» разделить / разделить потенциально смежные области памяти на маленькие разбросанные кусочки.
Стек использует этот ориентированный на память фокус, чтобы избежать этого и потенциально хранить в нем любую возможную смешанную комбинацию экземпляров определенного пользователем типа. Использование стека больше - хорошая идея, когда это возможно, так как его верхняя часть почти всегда находится в строке кэша, но мы также можем спроектировать распределители памяти, которые имитируют некоторые из этих характеристик без шаблона LIFO, объединяя память между разнородными типами данных в непрерывные куски даже для более сложных моделей выделения памяти и освобождения.
Современное оборудование разработано таким образом, чтобы быть на пике при обработке смежных блоков памяти (например, многократный доступ к одной и той же строке кэша, к одной и той же странице). Ключевое слово там - смежность, поскольку это выгодно, только если есть окружающие данные, представляющие интерес. Таким образом, ключом (но также и трудностью) к производительности является объединение отдельных фрагментов памяти снова вместе в непрерывные блоки, к которым осуществляется доступ полностью (все относящиеся к ним данные имеют отношение) до выселения. Самым большим препятствием здесь может быть система богатых типов, в особенности определяемых пользователем типов в языках программирования, но мы всегда можем обойти и решить проблему с помощью специального распределителя и / или более объемных конструкций, когда это необходимо.
уродливый
«Гадкий» сложно сказать. Это субъективная метрика, и тот, кто работает в очень критичной для производительности области, начнет менять свое представление о «красоте» на концепцию, которая намного более ориентирована на данные и фокусируется на интерфейсах, которые обрабатывают вещи в большом объеме.
опасно
«Опасный» может быть проще. В целом, производительность стремится достичь низкоуровневого кода. Например, реализация распределителя памяти невозможна без достижения типов данных и работы на опасном уровне необработанных битов и байтов. В результате это может помочь сосредоточиться на тщательной процедуре тестирования в этих критичных к производительности подсистемах, масштабируя тщательность тестирования с уровнем примененной оптимизации.
красота
Тем не менее, все это будет на уровне детализации реализации. Как в ветеране масштабного, так и в критическом отношении к производительности «красота» имеет тенденцию смещаться в сторону дизайна интерфейса, а не деталей реализации. Это становится экспоненциально более высоким приоритетом, чтобы искать «красивые», пригодные для использования, безопасные, эффективные интерфейсы, а не реализации из-за связывания и каскадных разрывов, которые могут произойти перед лицом изменения дизайна интерфейса. Реализации могут быть заменены в любое время. Обычно мы выполняем итерацию к производительности по мере необходимости и как показывают измерения. Ключом к дизайну интерфейса является моделирование на достаточно грубом уровне, чтобы оставить место для таких итераций, не нарушая всю систему.
На самом деле, я хотел бы предположить, что ветеранов, сосредоточенных на разработке, критически важной для производительности, часто будут стремиться сделать основной упор на безопасность, тестирование, ремонтопригодность, просто ученик SE в целом, так как крупномасштабная кодовая база, которая имеет ряд производительности -критические подсистемы (системы частиц, алгоритмы обработки изображений, обработка видео, звуковая обратная связь, трассировщики лучей, механизмы построения ячеек и т. д.) должны уделять пристальное внимание разработке программного обеспечения, чтобы избежать утопления в кошмаре обслуживания. Не случайно, что самые удивительно эффективные продукты также могут иметь наименьшее количество ошибок.
TL; DR
Как бы то ни было, это мой взгляд на эту тему, начиная от приоритетов в действительно критических с точки зрения производительности областях, что может уменьшить задержку и вызвать крошечную неэффективность, и что на самом деле составляет «красоту» (если смотреть на вещи наиболее продуктивно).
источник
Не для того, чтобы быть другим, но вот что я делаю:
Напишите это чисто и ремонтопригодно.
Проведите диагностику производительности и исправьте проблемы, о которых вам сообщают, а не те, о которых вы предполагаете Гарантированно, они будут отличаться от того, что вы ожидаете.
Вы можете сделать эти исправления понятным и понятным способом, но вам нужно будет добавить комментарий, чтобы люди, которые смотрят на код, знали, почему вы сделали это таким образом. Если вы этого не сделаете, они будут отменить это.
Так есть ли компромисс? Я так не думаю.
источник
Вы можете написать уродливый код, который очень быстр, и вы также можете написать красивый код, который так же быстр, как ваш уродливый код. Узкое место будет заключаться не в красоте / организации / структуре вашего кода, а в выбранных вами методах. Например, вы используете неблокирующие сокеты? Вы используете однопоточный дизайн? Вы используете очередь без блокировки для связи между потоками? Вы производите мусор для GC? Вы выполняете какие-либо блокирующие операции ввода-вывода в критическом потоке? Как видите, это не имеет ничего общего с красотой.
источник
Что имеет значение для конечного пользователя?
Случай 1: оптимизированный плохой код
Случай 2: неоптимизированный хороший код
Решение?
Легко оптимизировать критичные для производительности фрагменты кода
например:
Программа состоит из 5 методов , 3 из которых предназначены для управления данными, 1 для чтения с диска, а другой для записи на диск
Эти 3 метода управления данными используют два метода ввода-вывода и зависят от них
Мы бы оптимизировали методы ввода / вывода.
Причина: менее вероятно, что методы ввода-вывода будут изменены, и они не влияют на дизайн приложения, и в целом все в этой программе зависит от них, и, таким образом, они кажутся критичными для производительности, мы использовали бы любой код для их оптимизации ,
Это означает, что мы получаем хороший код и управляемый дизайн программы, сохраняя его быстрым, оптимизируя определенные части кода
Я думаю..
Я думаю, что плохой код мешает людям полировать-оптимизировать, а небольшие ошибки могут сделать его еще хуже, поэтому хороший код для новичка / новичка был бы лучше, если бы просто хорошо написал этот уродливый код.
источник