Как стать автором
Обновить

Комментарии 189

У математиков есть такая позиция: "не используй в рассуждениях то, что не можешь доказать". Вот с перформансом, мне кажется, должно быть то же самое: не используй в своём коде что-либо, если не знаешь как примерно оно работает под капотом, какие оверхеды несёт и как это в случае чего оптимизировать.


Это не значит что надо сверлить оптимизацию покуда не упрёшься в скорость прохождения электронов по проводам. Но надо всегда понимать что любое удобство даётся не бесплатно. Нет языков программирования, в которых бесплатно создание нового класса, вызов лишней функции, наполнение данными лишней коллекции и лишний её обход — просто нет.


Разумеется, есть ряд оптимизаций, которые сокращают стоимость. Что-то новое в этом направлении пытается сказать Rust, но покуда его не знает каждый встречный — надо помнить что удобство не бесплатно. И надо понимать насколько именно и какое удобство не бесплатно для того чтобы, делать простенькие трейд-оффы, когда это возможно.


Да и вообще мне кажется что плохая производительность — она далеко не всегда из-за того, что где-то есть Один Большой и Толстый Ботлнек. Она вполне себе может складываться из сотен неоптимально написанных мест. Уменьшим же их количество!
Аминь.

Я пишу на python. Да, он красивый, удобный и ни фига не быстрый. Но зачем мне скорость, если я все равно жду, пока удаленный сервер подумает и ответит на кучу запросов?


Меня вполне устраивает, что полный аудит системы осуществляется за 20 минут, если мне это нужно делать раз в неделю. Зато код понятно выглядит и намного проще его поддерживать

Удаленный сервер, наверное, тоже на питоне написан? :)
Это ноды Hashicorp Vault. Они неторопливые. Хотя, флуд в 50 мегабит запросов они спокойно пережили.
НЛО прилетело и опубликовало эту надпись здесь

Ну это всё-таки не статика. Это не самые лёгкие API запросы, которые невозможно кешировать.

НЛО прилетело и опубликовало эту надпись здесь

Есть некоторая ирония в том, что статья осуждает


Резкие, самодовольные фразы в стиле "этот болван четыре раза пробежался по коллекции, хотя можно было один", и тому подобное.

а Ваш комментарий, который, мне кажется, напоминает фразу "эти болваны, наверно, написали сервер на питоне, хотя можно было взять <высокопроизводительный язык>", получает только положительные оценки.


Допускаю, что я предвзят (сам люблю питон), и Ваш комментарий не осуждает использование питона в серверной разработке.

про питон сейчас наблюдаю картину у коллег:
когда-то давно начали они кодить свой софт на питоне, теперь он вырос и требует при любом масштабировании сервер в 64 ядра… а переписывать код видимо долго и ненадежно из-за костылей…
НЛО прилетело и опубликовало эту надпись здесь
где кто либо задумывался бы о performance

performance это не столько вопрос сколько железа нужно, но и как быстро вы ответите на запрос клиента. Есть множество ресурсов (букинг отелей, рекомендации стиминговых сайтов), где нужно за считанные миллисекунды дать ответ со сложнейшей логикой. Вот тут просто поставить больше серверов — не спасает (точнее спасет не всегда).
НЛО прилетело и опубликовало эту надпись здесь

И есть геймдев, где само словосочетание "считанные миллисекунды" режет ухо, потому что их на всё про всё в кадре всего 16.

А что с однопотоком делать?
Интел и Амд наращивают по капли скорость ядра, и каждое дальнейшее ускорение дается со все большим трудом.

99% криво написанного кода упирается в 1 ядро.
Частоты с 2002го года выросли в серверах по сути на 0 ГГц.
Я много где видел квадратический код. Так что железом тяжелый говнокод не исправишь.
Хорошо вам там в стране розовых пони и бесконечных ядер. У нас на железе бывает 16/32МГц и 4/8КБ памяти.

Так это и есть основная область Python: I/O-bound задачи. Особенно если учесть такие вещи как GIL.

Вообше то есть zero cost abstraction называется деляет как раз то что вы говорити, еше compile time execution тоже делает о чем говорите, еше constant propagation.
Ага, примерно наоборот работает компилятор, говорят оптимизация спичек, но компилятор это один большой оптимизатор спичек. Проблема давно уже не в области трейд офов и чего то еше, это всеравно что ставить себя равным богу. Максимум что вы можете делать очень сильно предпологать о том что вы пишите.
И вообше смех без причины признак дурачины. loop fusion способен «схлопывать» учитывая баунды беспонечно колличество циклов. В текущем виде мы выглядит как древние племена шаманов. Обсуждения разумности проходов по коллекциям и тд давно не имею никакого прикладного смысла. Вы либо это понимаете, либо нет. Контектуально это может иметь значения но никто в здравом уме не будет это обсуждать в отрыве от компилятора и задачи. Текущие действительно важные проблемы разработки ПО лежат не в области «он написал 4 цикла» они лежат в области — компилятор сказал что это оптимально но он вообше то может оптимальней если вы предоставите дополнительные гарантии рано или поздно все больше будут получать продукты предоставляюшие обратную связь программисту, подсказки. Системы структуризации ПО основанные не на текстово представлении данных а используюшие напрямую интеграцию с компилятором статическим анализатором и тд. Уже сегодня есть стандарты для унификации разработки ланг серверов, я думаю тенденция продолжится в реальности проблема в том, что этим никто особо не занимается ввиду сложности и наукоемкости.
Я дико извиняюсь, но из-за такого огромного количества грамматических и синтаксических ошибок я не смог дочитать коммент. Возможно в комменте была какая-то хорошая идея или толковое объяснение чего-то, но это все потерялось за стеной ошибок и плохим изложением собственных мыслей. Еще раз простите.
Вы реально думаете, что скорость распространения сигнала зависит от скорости движения электронов по проводнику?
я, например, точно знаю, что зависит, постоянные проблемы, когда после компиляции регистры оказывают в разных частях чипа — сантиметр и уже всё — разваливается…
приходится в ручную сбивать байты в одном месте
а почему не должно? 10Гбит — это 0.1нс, или 3см в вакууме, а меди меньше…

Можно поинтересоваться. Что вы имели ввиду когда писали "когда после компиляции регистры оказывают в разных частях чипа" ?

это когда проект компилируешь для программируемой логической схемы (которая может включать в себя как процессоры, так и память, мосты, интерфейсы, счетчики) и оказывается, что после трассировки Kбит Nбитного слова оказывается в одном месте чипа, а один бит как паршивая овца в другом, при попытке их одновременно за такт засчелкнуть этот бит не успевает на один такт, так как сигнал от него распространяется дольше (ёмкость, конечно, проводников влияет, но и сопротивление так же, тк постоянная распространения t ~ c*r+расстояние на/скорость света в материале )…
более того, когда нужно протащить на большой частоте сигнал по чипу из одной части в другой, приходится один или mраз простробировать его по пути в промежуточных регистрах, тк он не успевает добежать за один такт через весь чип.
Цитата на ум пришла. «Нет не решаемых проблем. Есть ленивые разработчики»
С посылом, думаю, вы согласны:))

Ради справедливости: "технически можно сделать всё" — довольно инфантильная и глупая позиция. Знаю ребят, которые так делали. Их продукт в итоге превратился в нелогичное и неюзабельное говно. Потому что на любую идею говорилось "да мы! да щас как! да мы можем!". А потом выясняется что технически нереализуемые углы, подразумевающие заглядывание в голову пользователя сглаживаются самыми нелицеприятными методами.

Цитата не полная, "технически можно сделать всё, вопрос только в требуемых ресурсах".
Чтобы узнать зависнет или нет программа, может понадобится подождать вечность. Мало кто готов столько ждать.

Вспомнилось про точный прогноз погоды на завтра, который надо ждать 3 дня…
«Нет не решаемых проблем. Есть инсульты в 30.» Кстати одна из интерпритаций
Прикольная статья )

Только вот про фронтендеров немного не всё правда. Уже давно есть другие виды коллекций: Map, Set. Есть их оптимизации для GC: WeakMap, WeakSet. К тому же есть proposal в спеку про Record и Tuple. Т.е., однозначно прям так говорить лучше лишь про старые версии. Ведь JS один из самых динамично развивающихся ЯП в последнее время )

К тому же причислять Object к коллекциям тоже не совсем верно. Скорее это вынужденное использование встроенной возможности создать пары «ключ»~«значение», где ключом будет строка и с недавнего времени Symbol, да и Array — это же тоже по сути Object, просто с другими ключами. Т.е. на мой субъективный взгляд правильней было бы написать «где до недавнего времени не было ни одной настоящей» коллекции )
ага, только в наколеночных бенчах простой Object почему-то оказывается быстрее Map, и смысла использовать Map практически нет, за исключением ситуаций, когда вот точно нужно.

Справедливости ради, наколенные бенчи — абсолютно не показательны. Но в то что у них Map медленнее Objecta отдает по ключу — меня точно не удивит)

насчет простой отдачи не знаю.
Я смотрел конкретные свои кейсы на предмет того, есть ли польза использовать Map вместо привычных коллекций. Оказалось что нет.
WeakMap — совсем другое дело, нужная и полезная вещь.

А как смотрел? Я вот покопал немного тему бенчмарка, и понял, что тут или хорошо умеешь — или не берись. Результаты на дебаге и на релизе абсолютно разные. Результаты на разных процессорах абсолютно разные. Любое приложение, например WebStrom, работающее во время бенчмарка, способно влиять на результаты этого бенчмарка непредсказуемым образом. При разной температуре процессора результаты могут радикально отличаться. На одних размерах данных будет побеждать один способ, на других — другой. А потом ты опят увеличиваешь размер данных, и результат снова противоположный. Т.е. я говорю не о маленьких погрешностях — результаты могут быть буквально противоположными из-за такой мелочи, как одно запущенное стороннее приложение. И это — верхушка айсберга.

я также понял, что очень важно — белый ноутбук или черный
НЛО прилетело и опубликовало эту надпись здесь

А он не шутит и не сарказмирует. Если ноут черный и лежит на солнце, термальный тротлинг может спутать все карты

С другой стороны, чёрный ноутбук в темноте лучше охлаждается излучением. Правда в плотной земной атмосфере это мизер.
да, я смотрел подкаст )
Замерял время работы сложного маппинга в своем приложении, показалось что можно ускорить с помощью Map, переписал, погонял на трех разных компах, под разными ОС, в разных браузерах. Получилось что Map везде чуть медленней объектов, даже при использовании delete.

А еще keys, entries у Map возвращает итераторы, в отличие от Object.keys, который возвращает массив, и становится возможным сразу использовать методы перебора массива. С мапом придется или Array.from или for..of.
Хотя логично, что мап предназначен для других целей, проходить по ним нужно довольно часто.
При этом еще объекты легко сливать и замораживать.
Map же имеет преимущество — метод clear.

С тех пор Map практически не использую, кода больше, преимущества по скорости нет.

У Map есть одно иногда очень важное преимущество — в нем ключами могут быть другие объекты сами по себе.
Т.е. мы можем использовать внешние объекты как ключи ничего не зная про их структуру и уникальность...

спасибо за ценное замечание, никогда бы не подумал что можно в mdn заглянуть, и вообще js он такой загадочный, объекты как ключи, ну надо же.
WeakMap — совсем другое дело, нужная и полезная вещь.

А как вы тогда использовали WeakMap?


The keys must be objects and the values can be arbitrary values.

На самом деле, вопрос, и правда меня интересует. Сколько не пишу, не было задачи, где WeakMap был бы решающим

Это была ирония, потому что это уже четвертый коммент, который рассказывает про ключи, хотя это очевидная вещь.

WeakMap удобен если вам нужно связать скажем одну сущность с другой, например в списке.
Допустим список юзеров, каждому юзеру сопоставляется объект деталей через WeakMap, если юзер больше не нужен и удален, то соответстующие детали тоже удалятся из памяти.

Можно считать это способом добавить юзеру приватные переменные, и при этом не вносить изменения в тип.
И не надо строить связи по соответствию по id, например, заводить вспомогательный хэшмап.
Ну как бы, Map позволяет в качестве ключей использовать что угодно. А Object только строки (и символы). Это главнейшее преимущество Map.
Вторым важным преимуществом является очевидность намерений программиста. Object это объект, а не Map. Так стоит ли использовать его в качестве Map, если есть настоящий Map? Многие JS-программисты вообще привыкли использовать окольные пути для реализации всего и вся. Получается такое Hack Oriented Programming. У этого явления исторические причины, и не хочется никого осуждать. Но на мой взгляд, сегодня, это просто дурная привычка, от которой надо избавляться. Я сам раньше складывал значение с пустой строкой, чтобы преобразовать его в строку. Потом начал писать String(val).
Что касается оптимизации, то вроде бы Map больше оптимизирован на частое добавление/удаление элементов, в отличие от Object. Так подсказывает Интернет. Но лично я это не проверял.
НЛО прилетело и опубликовало эту надпись здесь
Код субъективно красивей, и в некотором смысле «типизировать» проще )

if (myMapInstance.has(something)) ...


Читается гораздо луче чем:
if (myObjectAsCollection[something]) ...


И ещё, т.к. в случе с `Map` для проверки нужно вызвать метод, то добавляется дополнительная степень свободы, ведь раз `.has()` является методом, то его можно напрямую каррировать, а не изобретать велосипеды с Proxy )

С «типизацией» же всё становится немного ещё интересней, вот:

(new Map()).constructor.name === 'Map';


В то время как:
({}).constructor.name === 'Object';


И вот когда если это смешиваешь с наследованием через `extends`, то получается Номинальная Типизация «искаропки» )

А если хочется в бенчмаркинг, то опять же, нужно же понимать, что поиск в Hash-таблице по строковому ключу радикально отличается от поиска по произвольному типу ключа с вызовом метода. Т.е., использовать какую-нибудь объектную сущность в качестве ключа у объектов и их производных `{}` просто тупо нельзя же, а в случае с Map — да пожалуйста, делай что хочешь. )
Поясните про карирование, причем тут Proxy.
С типизацией же объекты предпочтительней, их можно сразу конструктором создавать и проверять через instanceof.
Впрочем обычно я этого не делаю, есть typescript или утиная типизация в рантайме, в классах хранения нужны обычно нет.
Через instanceof проверять имеет смысл только то, что имеет объектную природу, т.е. наследуется от типа Object. Но, instanceof не всегда показывает то, что хочется, он всего лишь проверяет ссылку на прототип. Т.е., мне подумалось, что зная .constructor.name мы могли бы как бы делать дополнительную проверку, т.к. это свойство устанавливается по имени конструктора, если оно задано. Т.е., если запретить анонимные классы и анонимные функции использовать в качестве конструкторов, то можно с этим поиграться. Но это «такое», да… Да и зачем, если оно уже встроено же в TypeScript на этапе компиляции, как вы верно заметили )

Про Proxy и каррирование: тут всё ещё хуже. Насколько я понял, основная суть вопроса была через Object получить такое же поведение, как есть у Map. В этом случае придётся повозиться, но всё равно ничего толкового не получится )

Каррирование используется для методов же. Т.е., если есть метод то его легко каррировать. И, т.о., наследуя от Map мы можем сделать override для .has, а, значит, мы получили дополнительную степень свободы. В случае с Object это сделать тоже можно, но, если свойства нет, то поиск устремится в прототип и в итоге дойдёт до самого конца, т.е. до null. В этом случае сделать аналогию с .has будет сложней, т.к. если мы хотели бы иметь возможность проверять наличие в структуре коллекции вещей, которые являются не строковыми ключами, то в конец цепочки прототипов придётся поставить Proxy, а сами эти вещи хранить как-нибудь отдельно, иначе же они так и окажутся строковыми ключами. Например какую-нибудь такую лютую дичь:

const myObj = {};

const nonStringKey = function () {};

const myObjNonStringKeys = [nonStringKey];
const myObjNonStringValues = ['nonStringValue'];

const proxyForObjectNonStringKey = new Proxy(myObjNonStringKeys, {
	get (target, nonStringKeyAsString) {
		// accessor nonStringKeyAsString может быть только строкой
		// поэтому придётся проверять ключи, как строки
		// можно попытаться что-нибудь придумать с хешированием
		// но это отдельная, ещё более жуткая история
		// ничего кроме строк же...
		
		let valueToReturn;
		myObjNonStringKeys.filter((key, index) => {
			if (
				key.toString istanceof Function &&
				key.toString() === nonStringKeyAsString
			) {
				// примерно здесь каррирование реализуем
				// но для примера просто вернём значение
				valueToReturn = myObjNonStringValues[index];
			}
		});
		return valueToReturn;
	}
});

Object.setPrototypeOf(myObj, proxyForObjectNonStringKey);

const s = myObj[nonStringKey];

console.log(s); // 'nonStringValue'



То есть даже с Proxy безошибочной ситуации не получится… И к этому коду придётся добавить ещё тонну всего, чтобы конвертация строкового ключа «на входе» могла хоть как-то работать. Но всё равно ничего нормального не выйдет, т.к. тут уже overhead такой, и качество кода такое, что ни один вменяемый code-reivew не пройдёт ни при каких условиях, даже если очень надо )
в случае нестроковых ключей все понятно, огород городить нет смысла.
Насколько я понял, основная суть вопроса была через Object получить такое же поведение, как есть у Map.

не, этого не нужно. зачем? В подавляющем большинстве случаев ключи таки строковые.

А вот с типизацией все равно не понял, чем имя конструктора лучше сравнения с самим конструктором.
Да и new Map() instanceof Object //=> true, с ним все тоже самое, что и с обычным объектом.
Так, эмм, теперь уже я не понял видимо:

1. Проверка на instanceof — это НЕ сравнение с конструктором, это сравнение с прототипом, то, что свойство .prototype вернёт: раздел "`instanceof` lies" тут. И ещё при этом если нужна проверка, то должна же быть ссылка на конструктор где-то рядом же )

2. Имя конструктора невозможно изменить, это readonly свойство. Т.о. анонимные функции и классы лучше не использовать.

3. Для того, чтобы использовать не только лишь имя, можно использовать дополнительно проверку по Symbol.hasInstance. Придётся немного повозиться, но в целом — почему нет )

То, что new Map() instanceof Object //=> true — это, надеюсь, понятно теперь, что это просто проверка не совсем правильная? Т.е., так можно проверить, что это нечто имеет в своём составе прототип от Object. Но это не то, что мы бы хотели получить, верно же?

Эмм… раз вопрос мне, то, видимо ничего страшного не будет, если я дам ссылку на немного дополнительных странных мыслей по этому поводу тут.
  1. Через defineProperty всё прекрасно меняется.
Спасибо, Да, ввёл в заблуждение )
Но side эффекты так себе :)

const MyConstructor = function () {};
const myInstance = new MyConstructor();
// next line: readonly works
myInstance.constructor.name = 'zzz';

// next 2 lines: MyConstructor
console.log(myInstance.constructor.name);
console.log(MyConstructor.name);
Object.defineProperty(myInstance.constructor, 'name', {
	get () {
		return 'всё прекрасно меняется';
	}
});

// next 2 lines: всё прекрасно меняется
console.log(myInstance.constructor.name);
console.log(MyConstructor.name);

А это сервис какой-то или просто кто-то делал бенч и обернут результаты в красивую картинку?

Сервис, картинки кликабельны.

О, оно даже на $mol работает! Прикольно! Только вот поля ввода как-то неадаптивненько выглядят.
Интересно, как там работает сама проверка: на беке или на фронте?


А вот сайтик hyoo.ru свёрстан и задизайнен очень спорно и "прибит" к левой части экрана(


</фидбек о котором никто не просил>

На фронте, конечно.


Сапожник без сапог, да.

Если на фронте, то это очень так себе. Получается, что условия не стерильные

Много к вам заходит стерильных пользователей?

Полагаю, большинство пользователей как минимум имеют куда более ограниченную машину, чем я

И многие из них заходят к вам с бэка?

Так на беке можно развернуть усредненный рантайм. Но главное тут не это — стерильные условия дают воспроизводимость.

За v8 скажу, Object там медленней будет, пушто при добавлении нового ключа будет создаваться новый хидден класс.
НЛО прилетело и опубликовало эту надпись здесь

Интересно. В js классы — это же сахар над прототипами, насколько я знаю

Более того, классы можно очень легко транспилировать до прототипов, «сахар» по своей сути очень простой.
Так что как и обычно в случаях а-ля «что-то сделал, что-то поменялось», проблема скорее всего была не там, где считает комментатор выше.
НЛО прилетело и опубликовало эту надпись здесь
Самое главное, чтоб потом во фронтэнд не приходили б люди, которым страшно 10К элементов обработать, потому что «веб тормозной».
Потому что мне приходится ходить и бить их по рукам, чтоб они не начали пытаться унести весь код на бэк и поставить раком всех юзеров с плохим каналом связи, или там, не пытаться приставить wasm (они чё-то такое слышали, что там всё lightning fast, не то, что этот ваш js) к коду.
НЛО прилетело и опубликовало эту надпись здесь
Вот вы и ждете получения этих комментов. Причём сразу же в виде html. К их обработке это всё не имеет никакого значения, её тут просто нет.
Если б вы получали комменты порционно и лениво, да еще и не в html, а как сырые данные (меньше объем) — js-кода на вашей машине работало бы на порядки больше, зато комменты начать читать можно было бы сразу, а не после обеда.

ЗЫ: А еще очень прикольно работать с нагруженными UI (а не сайтиками для потребления информации) в режиме «открыл сейчас — начал работать после обеда».
Причём сразу же в виде html. К их обработке это всё не имеет никакого значения, её тут просто нет.

Вы недооцениваете масштаб трагедии..


Есть большая разница между умением оптимизировать и нежеланием экономить на спичках.


Код, который обрабатывает 10К элементов, я буду писать вдумчиво и с оптимизациями. А код, который обрабатывает 10 элементов, я напишу так, как проще и понятнее. И всем того же желаю.


Если есть два алгоритма, один из которых O(N^3) а другой O(M log M), вполне может оказаться, что оптимизировать надо второй — потому что M >> N.

Как вы думаете, авторы Хабра предполагали, что под одной статьёй может скопиться более 2 тысяч комментариев?

лично у меня при большом количестве комментариев написать свой становится нереально, все тормозит при минимальных действиях.
Видимо такая защита от переполнения…
Полагаю, проблема в хроме. У меня в firefox работает нормально.

А почему бы они это не предполагали, причём с самого начала? На момент запуска Хабра в ЖЖ срачи на тысячу каментов вполне были. Думаете, авторы Хабра не надеялись на растущем рынке сделать что-то сопоставимое?

Предположить такое было бы несложно, я бы предположил :-)


Да на хабре-то как раз я проблем с этим не вижу. Ну, да, на атоме с полгигом памяти и на slow 3g будет не очень, но кого это волнует :-)


Есть некоторые несуществующие бложики, где и на моем core i9 при 2к комментах все тормозит ;)

https://habr.com/post/423889 Попробуйте в Хроме долистать до последнего комментария.

Ну не так уж и плохо, притормаживает, но жить можно.


Но "ленивый" рендеринг видимой области от где-то 300 комментов, конечно, не помешал бы, это я согласен.

А код, который обрабатывает 10 элементов, я напишу так, как проще и понятнее

А потом придёт кто-то другой, посмотрит "о, готовый метод" и скормит в него 10К элементов

Разумеется, у такого метода будет не generic название, а специфичное для конкретной задачи. А, скорее всего, и сигнатура будет такая, что скормить этому методу можно только объект совершенно конкретного узкоспециализированного класса. Или вообще это будет private method.

вместо тысячи слов

При начальной разработке всегда кажется, что код будет использоваться правильно. А потом прилетает новая фича и поехало

Потому что, очевидно, за качеством кода надо следить. Code reviews придуманы не просто так.

Естественно 90% кода лучше писать красиво, а не эффективно, но есть 10%… которые сейчас работают хорошо, а потом стреляют. Через год, в другом проекте, через утёкшую абстракцию. Когда уже никто ничего не помнит.
Несколько раз ходить по IEnumerable — это отличный способ выстрелить себе в ногу. Почему? А потому что сегодня это коллекция, а завтра это поток сообщений с удалённого сервера, который не остановить, и перемотка назад повторит все операции.
Да, это крайние случаи, и в статье про подобное сказано, но могу добавить следующее:
1. Когда пишется неоптимальный код, сразу надо думать — а есть ли возможность сделать его потом оптимально, если потребуется? Или мы в погоне за красотой должны всё переписать
2. Будет ли этот код кто-то поддерживать, или он уйдёт в библиотеку Common, которой пользуются все, но никто не помнит, что там есть и как работает
3. А может оптимальный но некрасивый код будет лучше читать? Всякие цепочки вида .Where.Select.GroupBy.Select.Where.Distinct.Any через некоторое время становятся совершенно непонятными, а тупой цикл for можно разметить комментариями и крутить в голове
4. Знаю ли я, как работает абстракция? Сколько она стоит? Возьмём Distinct — что там внутри? Какая сложность алгоритма? Не взорву ли я мир через 2 месяца?

Опять же, подчеркну, большинству кода это не надо. Но я неоднократно был свидетелем, когда вылезали жёсткие перфоманс проблемы на продакшен сервере (там данных больше и пользователи щаче ходят), которые было уже не пофиксить без серьёзного вникания в логику и переписывания больших кусков. И это времени занимало больше, чем подумать заранее и преждевременно оптимизировать.
3. А может оптимальный но некрасивый код будет лучше читать? Всякие цепочки вида .Where.Select.GroupBy.Select.Where.Distinct.Any через некоторое время становятся совершенно непонятными, а тупой цикл for можно разметить комментариями и крутить в голове.

Забавно, у меня опыт ровно противоположный: комбинаторы итераторов — это единственный способ сохранить читабельность, а вот цикл с увеличением числа строчек быстро превращается в big ball of mud.

У меня опыт простой: любой подобный код становится big ball of mud, но если выделены структуры данных — то в это можно въехать обратно с полпинка. Проблема циклов в том, что в них обычно порядочно «императивного шума» — всякого контента, который оказывает влияние на результат, но сам по себе смысла не имеет. В то время как в цепочке мапов/фильтров/итд лишнего ничего нет, у нас либо исходные данные, либо итоговые, либо один из этапов превращения первых во вторые, более ничего нет.
Всякие цепочки вида .Where.Select.GroupBy.Select.Where.Distinct.Any через некоторое время становятся совершенно непонятными

Понятнее некуда. Каждое слово в этой цепочке — некое конкретное преобразование данных.

а тупой цикл for можно разметить комментариями и крутить в голове

На каждое слово из этой цепочки тебе может пригодиться минимум по циклу. Итого, в твоём примере будет семь циклов.

Один цикл тут будет, 3 условия и 4 переменных.

На каждое слово из этой цепочки тебе может пригодиться минимум по циклу. Итого, в твоём примере будет семь циклов.

Ну почему же сразу 7? К примеру, первые три оператора: Where, Select и GroupBy (В зависимости от контекста), можно будет к примеру объединить в один цикл, осуществляющий одновременно фильтрацию, маппинг в объект и сохранение в какой-нибудь словарь. И скорее всего, если бы появились проблемы с производительностью, данная замена сказалась бы на ней положительно.
Where — скипается в самом начале цикла по if (....) {continue;}. GroupBy — видимо, мы ходим по двумерному массиву, ок, следующими строками мы на лету формируем ключ из части полей, и либо создаём новую запись в подготавливоемой структуре, либо увеличиваем значение в ней, если она уже создана. Ещё один Where — либо относится к ранее созданным композитным ключам — тогда проверяем при создании, либо к сумме значений — уже сложнее, это придётся действительно проверять ещё одним циклом. Distinct в комбинации с Any немного бессмысленен — сначала мы из предыдущей выборки берём только уникальные значения, а потом… одно из них, произвольное?

В общем, я не пытался доказать, что несколько циклов вообще не понадобятся — но их количество явно можно уменьшить, как и итоговую стоимость.
Намного веселее, в плане производительности, выглядят ситуации когда мы делаем запрос к базе данных, получаем результат, обходим его в цикле, и для каждого элемента делаем ещё один запрос за дополнительными данными. И, с одной стороны, я матерясь фиксил такие куски — с другой же, это писалось быстро, по задачам которые должны были быть готовы «ещё вчера» и не вызывало проблем год и более, пока данных не становилось слишком много.
Вот прям 1000 раз поддерживаю! Каждый раз, когда кто-то использует LINQ (и подобное) вместо простого for «потому что ну там же не важна скорость!» — он стреляет через портал, другой конец которого откроется в любое место и в любое время. Года через 2 этот портал может открыться и на ноге писателя, да. И это еще повезет, если на ноге, да.

Каждый раз, когда задача решается по принципу наименьших усилий — она обязательно возвращается потом обратно, спустя годы, но возвращается.

Не, конечно, если рассчитывать, что к тому времени ты уже уволишься…

А иначе — именно тебе же и придется потом чинить этот древний код. И это еще очень повезет, если прошло года 2. А вот когда ему лет 6-10 — это больно. Потому что с тех пор на нем наросло столько всего, что его банально страшно трогать, т.к. последствия вообще непредсказуемы. И сначала приходится выяснять, что там дальше происходит, кто и как использует проблемный код и на это уходит офигительное количество времени. И это еще сильно повезет, если не придется переписывать и дальше и дальше, когда получается снежный ком из правок.

А все потому что вместо цикла написали LINQ-запрос, следующий программист не стал вникать и прикрутил свой код сбоку рядом (что удвоило затраты ресурсов). Потом третий дописал код второго, всё это обернули в функцию, запихнули в десяток классов, а спустя годы оно наконец взорвалось, да…
Когда я вижу сложный Linq запрос, то мысленно проклинаю разработчика, который так написал…
Иногда это бываю я сам.
Каждый раз, когда задача решается по принципу наименьших усилий — она обязательно возвращается потом обратно, спустя годы, но возвращается.

Это, конечно, не так. В 99% случаев ничего и никогда не возвращается.

Я последние годы занимаюсь поддержкой сайтов. Так вот — даже если прилетает задача по какому-то левому сайту, которую сделали и забыли про сайт вообще. Даже тогда — он ВСЕГДА возвращается обратно, иногда спустя год-два. И самое печальное, что правки требуются именно там, где ты уже правил — ну просто потому, что клиент же не просто так просил там править, у него была цель и идеи и он продолжает их осуществлять дальше.

Сделал криво? Через год будешь делать нормально!
Если есть тесты — можно трогать на здоровье. Нет тестов — напишите их, рефачите и оптимизируйте на здоровье. Методология же проста:
1. Написать код
2. Написать тесты
3. Оптимизировать ТОЛЬКО проблемные места
Тогда и никакой легаси не страшен и перфоманс будет пофикшен уже по факту

А как вы пишете тесты на перформанс и как избегание регрессий в перформансе?

Все указанные выше тесты — это тесты на функционал куска кода, который собираемся рефачить. Тесты на перфоманс (не бенчмарки — локальные замеры) как правило уже пишутся уровнем выше.
А все потому что вместо цикла написали LINQ-запрос, следующий программист не стал вникать

Так проблема в LINQ-запросе или в том, что следующий программист не стал вникать?
Если бы там был цикл, а не LINQ-запрос, следующий программист стал бы?
Проблема не в LINQ вообще. Он больше как пример тут. Проблема в том, что если у вас главный критерий вместо читаемости и понятности (всем! даже молодым джунам!) кода — его красота, то рано или поздно кто-то не станет разбираться, что там в коде и просто возьмет «как есть». Собственно, дальше уже как повезет — вы могли написать хорошо и код будет работать без побочек. А могли — не очень хорошо (ну вы ж знали как код будет использоваться, че тут думать-то, смысл доп. проверки и оптимизации втыкать?) и вот тогда начнутся чудеса — и это будет очень хорошо, если начнутся они сразу, а не спустя годы.

Возвращаясь к LINQ: код не воспринимается при чтении (особенно если там что-то нетривиальное). Надо прям постоянно на нем писать, чтобы нормально читать такой код. Но хуже всего то, что LINQ по сути прям заставляет писать не оптимальный говнокод. Ну просто потому, что там каждый оператор — независимая сущность, которая изолирована от остальных. Там, где в обычном for'е можно сделать доп. проверки, оптимизации и хитрости — LINQ будет тупое последовательное выполнение. Иногда — это хорошо, жалко только, что в большинстве случаев такого не бывает.

Так проблема в LINQ-запросе или в том, что следующий программист не стал вникать?
Если бы там был цикл, а не LINQ-запрос, следующий программист стал бы?

Да, мы живем в реальном мире — программисты ВСЕГДА идут по самому легкому пути. Если у нас есть уже есть готовый LINQ-запрос (возвращаясь к примеру), то смысл его трогать? Он же может сломаться! Проще написать свои правки рядом — и вот у нас уже удвоение затрат на ровном месте. А имея простой цикл вместо LINQ — правки всунули бы в него, без удвоения. Опять-таки, надо понимать, что это пример и в реальной жизни все не так красиво. Но имея развернутый код шансы на то, что правки в него внесут оптимальным образом выше, чем если этот код скрыт под капотом.
Каждый раз, когда задача решается по принципу наименьших усилий — она обязательно возвращается потом обратно, спустя годы, но возвращается.

Весь код, не рассчитанный на работу в современных условиях, был очевидно написан давно. Вполне естественно к нему возвращаться и поправлять под новые требования. Ты просто правишь в том месте, где начало тормозить, и всё, а не переписываешь механически весь LINQ везде на циклы и т.п.


Ещё бывает, что потенциально медленный код до этого "спустя годы" не доживает — видоизменяется или заменяется другим. Во время написания он был нормальным и с работой справлялся. А про то, что сейчас он был бы медленным, ты даже не узнаешь. Узнаешь только про тот код, который дожил.


Оптимизировать что угодно можно до бесконечности, поэтому нужно иметь для себя какой-то ориентир, чтобы глядя на который можно было сказать, всё, хватит, код уже достаточно быстр, не надо больше тратить время. Часто когда что-то пишешь с нуля, такого ориентира либо нет (ещё нет), либо пока не напишешь значительную часть кода, не можешь сравнивать. Поэтому сначала пишешь быстро, просто и понятно; и уже потом, когда появляется представление о фактической производительности, об узких местах, начинаешь оптимизировать там, где в этом есть смысл.

Не, ну понятно, что оптимизировать просто чтобы оптимизировать — тоже не хорошо. Но и писать, заведомо зная, что написанное — медленное — тоже не очень хорошо. Одно дело, когда заведомо медленный код пишется для проверки фичи и в дальнейшем не будет использоваться, а другое — когда он сознательно пишется в продакшен «потому что так красивее». Я не против LINQ как такового, но надо понимать, где его стоит использовать, а где — лучше не надо. И когда для простейшего перебора и изменения элементов массивов начинают использовать LINQ, map и всякое такое, то это само по себе вызывает вопросы о компетентности программиста…

Как же хорошо, что я пишу на языке, где можно использовать LINQ-подобные цепочки функций и при этом не убивать перформанс

Не бывает в программировании абсолютного «медленно» и абсолютного «быстро». Бывает «достаточно быстро» и «недостаточно быстро», а несколько размытая граница между ними определяется обстоятельствами. LINQ вероятно не стоит пихать внутрь цикла из 10k итераций на кадр / 60 кадров в секунду ― 16 миллисекунд не резиновые. Но если код запускается при нажатии юзером на GUI-кнопку, то вообще плевать, за сколько он отрабатывает, за 1/10 микросекунды или за 10. Лучше будет тот вариант, который проще и понятнее. Если LINQ вдруг проще, то и пусть будет он, отличная же вещь.

Не, ну так-то LINQ прикольная штука, я согласен. Но изначальные примеры в статье не про юзерский интерфейс все-таки, соответственно и комментарии все как один — про то, что автор роет себе яму в будущем.

И даже в интерфейсе с таким подходом можно выстрелить себе в ногу, если не учесть особенностей реализации. Например, засунуть на форму красивый список чего-то, а спустя пару лет окажется, что в этом списке уже 10к значений и количество продолжает расти по экспоненте. Понятно, что всего не предусмотреть, но и пихать заведомо не оптимальный код под лозунгом «там все равно кнопку ждать — не страшно!» — это тоже не лучший подход, имхо.

Просто альтернативный подход ― это преждевременная пессимизация, когда ты стреляешь себе в ногу прямо сейчас специально и прицельно; вместо того чтобы мимоходом написать пару простых строк, от которых когда-нибудь в будущем тебе может прилететь, а может и не прилететь ― но даже если и прилетит, их не жалко переписать под новые требования.


Вот есть «ленивые вычисления» (привет, LINQ!), а это такое «ленивое программирование». Принцип одинаковый ― незачем делать сейчас то, что сейчас никому не нужно, и хрен знает, потребуется ли в будущем.

Согласен, да. Всё хорошо в меру.

Просто ко мне последнее время слишком часто стал старый код прилетать на правки, со всеми его ужасами. Невольно уже начинаю подкладывать соломку заранее — оно ж эдак снова прилетит потом
Знакома ситуация, когда есть обычная задача: бери list и делай задачу — все просто, большой нагрузки в этом месте нету: O(n) хватит.
Но нет, люди, писавшую большую часть жизни на чем-то низкоуровневом, и теперь везде ищут «проблему». Они готовы реализовать свой Linked list, Circular buffer, только чтобы выиграть 0.0001 секунду, но при этом сделать код максимально нечитабельным, а спустя n времени это будет боль его поддерживать.
Считаю, что если человек overинженирит в таких случаях — это абсолютное зло.

Или эти люди прошли собеседования в лучших традициях вопросов про алгоритмы и большего О. А потому и считают, что связанный список самая оптимальная структура данных в то время когда в реальном мире теория и практика иногда сильно расходятся :)

Большинство из тех, кто знает про большое О, почему-то не понимает, что оно имеет смысл, когда N стремится в бесконечность. В реальности же N=100, или даже 100k, от бесконечности сильно отличаются, и часто всё решают константы, а не O().

Особенно, когда вместо O(n^2) пишут O(2^n), которое при любом разумном соотношении констант будет занимать часы при уже n>40.


То же N^2 для N~100k уже невообразимо медленнее O(N log n) — тут никакие константы не помогут.

Поскольку O() тупо не учитывает константы, то O(N), O(N/100k) и O(N*100k) друг другу тождественны. Ну и как тут можно серьёзно рассуждать о производительности?

Потому что константа редко достигает таких экстремальных размеров.
Очень сложно придумать хоть сколько нибудь реальный алгоритм, который выполняет 100N операций. Вся константа зарыта в коде. Чтобы сделать 100kn операций, надо или цикл гнать до 100k, что на самом деле будет каким-то дополнительным параметром M, который тоже надо включать в анализ, или написать 100k строчек кода!


Константы меньше единицы вообще сложно получить, если только не игнорировать 99.99999% процентов данных, что тоже весьма специфический случай.

Очень сложно придумать хоть сколько нибудь реальный алгоритм… Вся константа зарыта в коде.

Вот. Константы в коде. Очень легко представить две реализации одного алгоритма, одна из которых для обработки каждого элемента входных данных совершает по времени в 100k больше бессмысленной работы, чем другая реализация.


O() ― это такая странная безразмерная штука про условные операции абстрактного исполнителя алгоритма в вакууме. Мы же пишем код для конкретного исполнителя, под данные определённого размера и характера; и время работы этого кода в секундах ― единственное, что нас реально волнует.


Знать про O() вообще полезно, но не написав код, только по двум разным O() делать какие-то выводы, это пальцем в небо и «преждевременная пессимизация».

Очень легко представить две реализации одного алгоритма, одна из которых для обработки каждого элемента входных данных совершает по времени в 100k больше бессмысленной работы, чем другая реализация.

А я ничего такого представить не могу. Ну кроме умышленного замедления, где над, собственно, кодом обернут цикл на 10k операций. Вы можете сделать в 10 раз больше работы, если очень постараетесь, будете очень не cache friendly, и не примените ни одной очевидной оптимизации.


O() ― это такая странная безразмерная штука про условные операции абстрактного исполнителя алгоритма в вакууме

На практике, если у вас N>30, то O(N^2) почти всегда медленнее O(N log N), а даже самая быстрая реализация O(N^3) медленнее даже самой дерьмовой реализации O(N^2) алгоритма. Опять же, если умышленно искусственно код не замедлять.

А я ничего такого представить не могу. Ну кроме умышленного замедления, где над, собственно, кодом обернут цикл на 10k операций.

Одна высокоуровневая "операция" без всяких циклов может разлагаться на непредсказуемое количество низкоуровневых. Никто не может сказать чётко, что такое "операция" в O() и чему она соответствует в реальности, и как результат, никто об этом и не говорит.


Можно читать и обрабатывать байты из файла по одному через какой-нибудь высокоуровневый поток, можно буферизировать ввод и двигать указатель по байтам в буфере, а можно читать и обрабатывать не байты, а пачки по 16/32 байта через SSE/AVX. Алгоритмически варианты не отличаются, а разница в скорости на многие порядки и определяется исключительно константами.


На практике, если у вас N>30, то O(N^2) почти всегда медленнее O(N log N), а даже самая быстрая реализация O(N^3) медленнее даже самой дерьмовой реализации O(N^2) алгоритма. Опять же, если умышленно искусственно код не замедлять.

Умышленно никто не замедляет, просто необходимость оптимизировать код, когда такая необходимость существует, регулярно упускается в пользу рассуждений об O(). Типа, N^2 лучше, чем N^3, значит соответствующая реализация должна работать быстрее и можно не напрягаться. А это O() описывает не реализации и не скорость, а исключительно поведение алгоритмов на максимально неудобных входных данных при росте их объёма.


Для двух конкретных реализаций можно построить два графика времени их работы в зависимости от объёма входных данных, сравнивать и делать выводы.


Графики O(n^2) и O(n^3) даже нарисовать невозможно, делать выводы и подавно. Единственный вывод, которых можно сделать, — существует такое неизвестно какое N, больше которого любая возможная реализация алгоритма O(N^2) будет работать быстрее любой возможной реализации O(N^3). Ну, не очень полезное практически знание. N-то у нас обычно задано заранее или мы уже представляем, в каких примерно пределах оно может гулять.

Одна высокоуровневая "операция" без всяких циклов может разлагаться на непредсказуемое количество низкоуровневых.

Приведите пример, когда "высокоуровневая операция" в 100k раз медленнее на практике?


Можно читать и обрабатывать байты из файла по одному через какой-нибудь высокоуровневый поток, можно буферизировать ввод и двигать указатель по байтам в буфере, а можно читать и обрабатывать не байты, а пачки по 16/32 байта через SSE/AVX.

На практике, опять же, операционная система, контроллер диска, страндартная библиотека — все будут какую-то буфиризацию производить. Что бы именно до побайтового чтения с диска допрограммировать, это надо что-то специально делать. Всякая векторизация — ускорение в 32 раза, а не в 100k.


Вы все правильные вещи говорите. Да, из-за константы O(n^2) алгоритм, теоретически, может быть медленее O(n^3). Но, позвольте мне перефразировать свое утверждение, чтобы предмет спора не терялся: Если какая-то задача имеет решения за квадрат и за куб, и n не тревиально маленькое (скажем, n>=30) — то хорошая реализация решения за квадрат будет быстрее хорошей реализации решения за куб. Потому что константы не различаются на много порядков. Исключения настолько редки, что знание ассимптотического анализа, все-таки, полезно.

Закольцованный буфер — ещё ладно; время потрачено, но он хотя бы работает. А на связном списке проще секунду просрать, чем 0.0001 выиграть.

Хорошее начало, плохой выбор плохих героев. Не надо было до этого скатываться.

В любой истории должен быть свой мудак. Ну вот, работает оно так, не я это придумал, и не мне это менять.

НЛО прилетело и опубликовало эту надпись здесь

Ну вот зачем так? Есть у меня и другие статьи без нытья, например
https://habr.com/ru/post/506088/

Вот так просто и легко раскрыл все свои карты… хмм)
Да ну, в этот раз я автора угадал по заголовку, ещё не открыв статью.
НЛО прилетело и опубликовало эту надпись здесь

Ну, в идеальном мире. В реальном мире, ну блин, ну не пишут эти требования. А если пишут, то недостаточно детально. А если достаточно детально, то они устаревшие. Я не знаю. Я работал в гигантских и крутых компаниях, где есть ресурсы на такие вещи — но даже там качественно расписанных требований днем с огнем не сыщешь.

Блин, если уж все пытаться в требованиях учесть, то можно прямо там код и писать.

О чем и речь

НЛО прилетело и опубликовало эту надпись здесь

Я разве про это говорил?

НЛО прилетело и опубликовало эту надпись здесь
У меня есть друг, опытный разраб высокого класса, который на днях выпнул человека с собеса, потому что тот предоставил медленное решение. Задача была — реализовать специфичный LINQ метод. В C# эти методы следует делать ленивыми — через итераторы и yeild — а парень просто зафигачил новый список, и стал пихать в него значения, чем сломал ленивость.

Если твой друг ориентировался тем, что решение — медленное, то, по-моему, это повод выпнуть и его с собеседования. Потому что проблема тут не в производительности, а в архитектуре. И если ты внутри LINQ-метода будешь создавать список, то сломаешь не только и не столько производительность, сколько саму идеологию LINQ.

Ты пишешь цикл foreach по сущности, возвращённой этим LINQ-методом. При нормальном ходе событий, на каждой итерации цикла дёргается итератор, внутри отрабатывает код до очередного yield, вытаскивая элемент откуда-то, как-то его обрабатывая, и выдавая тебе в цикл.

Ежели же парень решил запилить внутри список, то он таки создаст его на первой итерации цикла. И...? Тут у меня несколько идей, что он мог там такого запилить, и одна другой мрачнее. Возвращает итератор на этот список? Возвращает сам список как IEnumerable? Ну, ок, а на следующей итерации цикла что происходит? Создаётся список ещё раз или он как-то кешируется… но где?

Я повторюсь, здесь проблема не в производительности, а в полном непонимании азов LINQ.

Хотя, с другой стороны, часто ли кому-то приходится реализовывать специфичные методы LINQ? Я давно не помню, чтобы я этим занимался, хотя на дотнете регулярно пилю то десктоп, то веб. Тут можно было бы, наверное, просто не парить парня этой задачей, если на практике он этого не будет касаться, а будет пилить очередной CRUD — и всем бы было хорошо.

Да, непонимание философии LINQ — это проблема. Но само решение, которое рушит эту философию — не критичное. Вот например майки в доку положили именно такое решение, и не чешутся https://docs.microsoft.com/ru-ru/dotnet/csharp/programming-guide/concepts/linq/how-to-add-custom-methods-for-linq-queries

Это аггрегирующий метод, после него уже не будет обработки элементов по одному. Так что тут можно что хочешь использовать.

Это Median. Ниже есть AlternateElements, не агрегирующий

Ого, не заметил. Это явно пример плохой документации.

Да. Вот что-что, а дока должна именно философии учить.

Задачи разные бывают. Зачастую закешировать результат запроса в памяти клиента эффективнее, чем занимать ресурсы сервера.

Ну да. Об этом ещё Макконелл говаривал.


Во имя эффективности — при- чем достигается она далеко не всегда — совершается больше компьютерных грехов, чем по любой другой причине, включая банальную глупость. (Совершенный код, глава 25)

Задача была — реализовать специфичный LINQ метод. В C# эти методы следует делать ленивыми — через итераторы и yeild — а парень просто зафигачил новый список, и стал пихать в него значения, чем сломал ленивость.

Не всегда всё однозначно, например, если поискать в гугле 'C# enumerate collection batches', можно найти такой ответ
public IEnumerable<user> GetBatch(int pageNumber)
{
    return users.Skip(pageNumber * 1000).Take(1000);
}


И многие будут пользоваться, забывая что LINQ это обобщенная штука которая может применяться не только над стандартными коллекциями, и если попробовать применить такой подход к курсору над коллекцией в MongoDB, то получим нереальную просадку производительности, т.к. на каждой итерации Skip будет перебирать все предыдущие документы. Ленивые вычисления это отлично, но всегда нужно исходить из контекста задачи. И разные реализации ленивых вычислений тоже могут сильно отличаться.
Прощу прощения, но я не понял в чем проблема указанного кода.
Проблема в том что метод не хранит состояние (пр.: курсор в БД)? Так это нормально.
Или проблема в том, что нет нормальной обертки IEnumerable<IEnumerable>, которую можно адекватно перечислить? Даже если так, то курсор необходимо будет закрыть, то есть нам уже не подходит IEnumerable — значит, что приведенный пример с просадкой производительности по курсору немного грубоват. Ни IEnumerable ни IEnumerator не имеют методов Dispose или Close.
Да,
'C# enumerate collection batches',
— это слишком круто для такого кода.

А на самом деле мне просто хотелось бы узнать как связать запросы к БД и IEnumerable интерфейс.
В целом — согласен с автором, но не сосчитать сколько раз приходилось оптимизировать код, который был написан во времена, когда элементов в коллекции было 100. Проходит несколько лет, база растёт и сотня превращается в миллион. Всегда нужно смотреть вперёд.
Нагрузочное тестирование — очень полезная штука.
Два примера. На прошлой работе посадили человека за GIS-карту. Задача — отрисовать на карте маршруты движения транспортных средств, собранные с GPS-датчиков. Несколько месяцев человек старался, вкорячивал GIS-компонент, прикручивал отрисовку маршрутов поверх него. До продакшена всё выглядело более-менее работоспособно. Но как только пришло время подключить к карте реальную базу — механизм превратился в тыкву. Лёг насмерть. Ничего более сложного, чем отрисовка десятка тестовых треков в масштабах населённого пункта эта система показать не смогла, так как вся отрисовка шла синхронно в UI-потоке, к тому же все треки рисовались без прореживания и независимо от того попадают ли они в видимую область или нет. К тому же автор уволился. Всё было написано настолько в лоб, что проще было всё выбросить и начать с нуля, но он приколотил механизм к общей системе настолько надёжными гвоздями, что это оказалось почти невозможным. В итоге пришлось потратить ещё полгода на постепенный рефакторинг и оптимизации. А человек перед увольнением отмазывался тем, что ему никто не сообщал сколько транспортных средств должна поддерживать система и на его наборе данных всё работает хорошо.
Второй пример более свежий. На машину клиента загружаются строки данных и кладутся в локальную базу. Механизм рабочий. Изредка от саппорта доходят жалобы клиентов на неторопливость этого процесса, но проблемы некритичны. Со временем данные накапливаются и появляется несколько клиентов, у которых размеры локальных баз доходят до десятка гигабайт, при этом процесс синхронизации занимает уже несколько суток. На этапе проектирования системы никто не подумал, что через годы работы накопление данных приведёт к серъёзной проблеме с производительностью. На оптимизацию ушла пара недель, после которых двое суток превратились в пару часов.
Всегда нужно смотреть вперёд.
Всегда есть обратная сторона медали.
Делается фича, вроде нужная. Думаем — надо сделать хорошо и тратится на это дольше времени, чем сделать в лоб. Выкатывается в прод, снимаются метрики и решение — убрать фичу.

Таких примеров в разработке куда больше и случаются они гораздо чаще, чем проблемы производительности, пруф:
появляется несколько клиентов, у которых размеры локальных баз доходят до десятка гигабайт
Если бы сразу обдумывали все возможные нюансы, этих клиентов могло бы и не быть. А могли бы и быть.

Могут быть проблемы с производительностью, а могут и не возникнуть. Зайдет фича или не зайдет. Вот как это все сразу рассчитать? Если знаете как, поделитесь секретом
О, это достаточно просто. Нужно быть не просто кодописателем, но и немного бизнес-аналитиком и анализировать входящие требования. Помогать Product Owner правильно формировать требования с учётом знания особенностей работы системы. Заранее думать что произойдёт в тот момент, когда юзер пользуется фичей год, два, пять, десять лет. Проектировать фичи не под песочницу, а под продакшен.

Я стараюсь писать сразу оптимальный код, тем более с опытом издержки на такую реализацию практически равны нулю. В последнее время часто использую спаны для работы со строками — это тоже все несложно. И я редко использую LINQ, хотя и не против него, если другие участники команды его используют.


Ну и да — на моей практике встречалось, что код конвертера Objective-C -> Swift начинал тормозить из-за неоптимальных коллекций — приходилось менять обычные списки на HashSet или словари. Причем проблема проявлялась только на относительно больших файлах. А в одном месте отказ от yield наоборот помог ускорить производительности.

Так и осталось непонятно почему для автора медленный код — более «эстетичный». Без каких либо критериев это вкусовщина, «я — художник, я так вижу». В отличие от производительности, где есть чёткие критерии какой код лучше или хуже и насколько. Можно точно установить где «экономия на спичках», а где — не масштабируемое решение и будущее «бутылочное горлышко». Я бы ещё принял за аргумент более простой или менее многословный код, но «эстетичный» — это фиаско.

«Художникам» от мира программирования стоит подумать о смене профессии на действительно творческую, ИМХО. Ради себя и других.

Я бы вышвырнул из профессии людей, которые пишут нечитабельный и неподдерживаемый код.

Как только напишете эталон читаемости и поддерживаемости — начнём вышвыривать.

Ну я на самом деле тут ответил агрессией на агрессию. По большому счету, речь ни идёт ни о каких эталонах. Вопрос в приоритетах и трейдофах. Т.е. не так важно, читабельный код на самом деле или нет, важно, пытаюсь ли я сделать его читабельным.

Отвечать агрессией на агрессию — тупиковая ветвь диалога.
Ну вот один пытается сделать читабельно, а другой читать это не может — у него кровь из глаз течёт.

Да какой диалог. Меня задели, я насрал в ответ.


А по читаемости — ну да, так и есть. Но мы же это обычно обсуждаем. Вот у меня был кейс, что тимлид буквально хейтил вертикальные вайтспейсы. Не ставил их нигде, ещё и линтер настроил. В итоге нам удалось его переубедить. Потому что мотивация то у него была правильная — писать красивый и читабельный код.

Так и осталось непонятно почему для автора медленный код — более «эстетичный».

Логическая ошибка. Статья про то, что эстетичный код бывает «медленным», и что это не повод убивать эстетику, если скорость работы и так устраивает. Из этого утверждения не следует, что медленный код ― эстетичный.


«Художникам» от мира программирования стоит подумать о смене профессии на действительно творческую, ИМХО. Ради себя и других.

Как это возможно, программировать и одновременно не дружить с логикой?

С одной стороны вы правы, бессмертное «Make it work, make it right, make it fast» с другой мне тут месяц назад попался красивый и хорошо читающийся код для одной биологической симуляции, не имевший батлнеков съедающих больше нескольких десятков процентов: habr.com/ru/post/508304 я посидел с ней некоторое небольшое время и ускорил в 27 раз. Курочка по зёрнышку клюёт.
я посидел с ней некоторое небольшое время и ускорил в 27 раз

Неплохо! А где-то есть пост с процессом ускорения?
Так я же ссылку дал. В статье есть стрим на ютубе где я поазываю примерно половину ускорений с объяснениями, а оставшееся можно посмотреть по моим коммитам в репозитории.

Ну так всё ровно в соответствии с поговоркой.

В целом да, проблема в том, что у приложения не было ярко выраженного батлнека, Вместо этого было огромное количество мест, где терялось процентов по 20. Когда потому я его ускорял фактически пятая часть исходного приложения ушла под переделку. Хотя так всё равно лучше, чем пытаться писать с самого начала, хотя бы потому что если я косячил в рефакторинге (а я косячил), то изменившиеся результаты выполнения кода предупреждали меня об этом. Там много где случайные числа использовались и я просто выдавал в консоль состояние RND через 10000 цаклов симуляции. Если оно изменилось, значит ой.
Говорить про оптимизации на собеседованиях — неплохая идея. Однажды мне дали для разминки джуниорскую задачу, а я упомянул, что сравнение через charCodeAt быстрее, чем через charAt, ну и ещё что while(--l) работал раньше быстрее всего, но сейчас разницы с forEach нет. Больше меня ничего не спрашивали. )
Сложная эта тема «оптимизация» и много споров было. Сделать оптимизированно — дорого. Сэкономить вроде незаметно, а потом баз у тебя уже 30 процессов крутится и каждый думает «ну я чуть чуть тут ЦПУ отъем, ни кто и не заметит» и вот у тебя уже график загрузки ядер выше 80%.
Недавний пример — есть железо, капчит аудио в буфер в памяти, сотф его разбирает т.к. аудиосэплы упакованы в AES3, на 4х аудио потоках дышит хорошо, на 8 кашляет и вид имеет бледный. Инженеры говорят — проц слаб, нужно чтоб парсило ауидо поток железо! Точка!
Электронщики чешут репу и начинают что-то шаманить с поникшим видом, так как стоимость сделать «красиво» на их стороне существенно выше.
На следующем код ревью выясняется, что ради красоты доступ к аудио был орагинзован не по 4 байта, а по битам, кажый раз когда нужно было проверить бит — его снова ивзлекали из байта, с блекджеком и ночными бабочками масками и сдвигами.
Две крайности — оптимизировать все всегда и максимально или делать красиво — ни кто просадку производительности не заметит.
Первый дорог, второй имеет отложеный эффект, когда есть 100 функций, вроде каждая нормально работает, а все вместе уже сущесвтенно влияют и не пойми что оптимизировать, переписывать надо.
Рост мощностей CPU закончился, мода на производительный код будет медленно, но верно возвращаться.
Какой то баланс нужен, разумный :)

Что люди не делают, как не извиваются, лишь бы на Си не писать...

Как написать на C обобщённую сортировку, в которой компаратор не будет каждый раз вызываться по указателю?

Понятно, что каждый частный случай может быть оптимальнее, но в целом в чем проблема компаратора-функции? Call/ret же линейные?

НЛО прилетело и опубликовало эту надпись здесь

Макрос это статически на момент компиляции. Может имелось в виду в рантайме… я бы тоже послушал лекцию как это должно быть.

Можно и так. Но это неудобно по очень многим причинам

лишь бы на Си не писать...
Спасибо, но не надо. Си подарил огромное количество уязвимостей и падающих программ.

Ну а что вы хотели, когда со всех сторон перинка не постелена — выстрелить в ногу гораздо проще...


Тем больше разница отличающая хорошего программиста от обыкновенного.


Понятно, что бизнесу лучше перинки. Пока в жесткие тиски не зажали.

Ну а что вы хотели, когда со всех сторон перинка не постелена — выстрелить в ногу гораздо проще...
Уже был cyclone, сейчас rust, однако до сих пор предлагается писать на си, особенно на opennet или на лоре.
Тем больше разница отличающая хорошего программиста от обыкновенного.
Нет никакой разницы. Ладно, если бы ошибки были в проектах школьников, а так они практически везде, посмотрите блог psv-studio.

Pvs studio не плюсы ли проверяют?


Си это база, основа, глыба — есть огромное коммьюнити, десятки лет граблей и прочего. Где стрельнет в ногу раст если на нем ssl начнут писать — может увидим. А может и нет...

Pvs studio не плюсы ли проверяют?
Там четыре языка С, C++, C# и Java.
Си это база, основа, глыба — есть огромное коммьюнити, десятки лет граблей и прочего.
Си, это прекрасная возможность писать код в 2020 году так, словно на дворе семидесятые. И на всё те же грабли можно наступить, за прошедшие десятилетия их никуда не дели. Как тогда, когда while(*to++ = *from++) ;считалось верхом изящества, можно было запросто затереть кусочек памяти, так и сейчас.
Где стрельнет в ногу раст если на нем ssl начнут писать
Насколько я понимаю, в этой библиотеке всё равно будет использоваться ассемблер, для ускорения работы, так что это не показатель. Сравнивать имеет смысл программы на си и на rust. Вот пример, когда переписывание магическим образом заставляет работать %include с тремя уровнями вложенности.
Подскажите, а на хабре, случайно, нет возможности скрыть все статьи определенного автора? Придётся userscript писать?

Достаточно в них просто не заходить. Но вас ведь не сами статьи бесят, вы недовольны их рейтингом и положением — и вот с этим-то вам ничего не поделать, сколько скриптов не напиши

Мне не хочется каждый раз в ленте ник автора искать. А так только время зря потратил — заголовок интересный, первые абзацы вполне со смыслом. И только потом понимаешь, что это очередной неподкрепленный ничем поток оскорблений от короля набросов на вентилятор.

кто допустил умственно-отсталого к программированию?
Откуда нам знать, кто допустил вас к программированию?
я и не программирую, просто я знаю чего я хочу — чтобы куча программ которые мне нужны, реагировали мнгновенно и потребляли минимум энергии(что не возможно без минимизации времени исполнения, ЦПУ и памяти), а при подходе ТС такого не будет в принципе(и не надо про подходящее железо). типа пример от MS (https://www.nexusmods.com/skyrim/mods/92725) — очень похоже на действие ТС.
НЛО прилетело и опубликовало эту надпись здесь
Нет ничего страшного когда ты проходишься по коллекции из 10 элементов. Кривой код — это когда ты лишние данные тащишь из базы
НЛО прилетело и опубликовало эту надпись здесь

Нет, даже не так. Зачем писать код, который ходит по 10 элементам, если можно — по бесконечности!

НЛО прилетело и опубликовало эту надпись здесь
Всё так. Код должен быть максимально понятным для человека, наиболее очевидным, наивным, в лоб. Если он после этого тормозит, можно заниматься оптимизацией. И то, оптимизацию следует начинать не с выбрасывания понятного кода, чтобы заменить его «быстрым» на битовых сдвигах, а с разработки абстрактных инструментов, которые оптимизируют код, написанный наивно, и только если это не получится — пачкать непосредственную логику.
НЛО прилетело и опубликовало эту надпись здесь

Краткое содержание статьи: «что лучше: табурет, или апельсин? — ответ: у табурета три ноги, а апельсин — круглый».


Термин «преждевременная оптимизация» в обиход ввел Дональд Кнут, одной максимой, которая легко гуглится, и которая, разумеется, разносчиками была полностью вырвана из контекста. Кнут имел в виду совершенно не то, что поняли и продолжают понимать люди, знакомые только с самим лишенным контекста мемом. Я полагал, что прямая ссылка на полный текст появится в заметке, полностью посвященной этой фразе, но если автор не удосужился дать ссылку, то и я не стану.




Вот правильный тезис, который занимает 2 строки — вместо воды на сто абзацев из заметки выше.


Неоптимизированный код писать можно, и нужно, но только в двух случаях:
  • если доподлинно известно с гарантией 102%, что объемы обрабатываемых этим кодом данных — постоянны во времени, и никогда не вырастут,
  • если четко понятно, как этот код впоследствии, когда объемы вырастут, можно будет изменить малой кровью без необходимости переписывания с нуля.

Во всех остальных случаях эффективность не должна страдать в угоду «высокой читаемости», а сторонников тезиса «код чаще читают, чем пишут» — надо просто увольнять за профнепригодность.

Быстрый секс — вообще не проблема, если ты знаешь как его продлить. Главное красиво.
Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории