Если вы занимались профилированием своего приложения, то, глядя на CPU Flame Chart, вероятно испытывали смесь досады и азарта, глядя на особо «жирный» метод. Досады – что ваша программа всё ещё не идеальна по скорости. Азарт – от того, что вы можете докопаться до причины проблемы и отжать для процессора ещё немного свободного времени на более полезные вычисления. По крайней мере, я регулярно становлюсь жертвой обоих этих чувств, чему данная статья и обязана своим появлением.  Мой кейс – это попытка выжать из игрового движка Flame больше скорости и возможностей, чем он может «из коробки». Гейм-разработка имеет свои особенности по сравнению с «парсингом большого json» или устранением подлагивания при разовом проигрывании анимации, как минимум потому что здесь потенциально объёмные вычисления производятся абсолютно на каждом кадре. Так что, наверное, мой опыт не сильно будет перекликаться с теми проблемами, которые встречает большинство Flutter-разработчиков. Однако хочу отметить, что описанные здесь подходы могут работать не только для Dart, а вообще для любых языков, особенно высокого уровня – нужно только уточнить детали, как именно в вашем языке реализованы те или иные структуры данных, и исходя из этого выбрать оптимальный метод работы с ними.  Итак, допустим вы открыли Flame Chart и видите какие-то огромные forEach, неэффективные HashSet и HashMap: Обращу внимание, что по большей части эти функции – последние в цепочке вызовов. То есть медленная работа не обусловлена какой-то кастомной логикой внутри цикла, а самим количеством этих циклов.  Конечно, когда на графике на первые места выплывают «внутренности» движка и приватные методы встроенных структур данных, первым шагом стоит посмотреть, а можем ли мы как-то уменьшить количество циклов? Здесь решение зависит от вашего конкретного случая. Беда в том, что это может не решить проблему, и набивать массив кучей элементов, а потом неоднократно в цикле по ним ходить может оказаться просто необходимо для логики вашего приложения. Как, например, в игровом движке – там просто нельзя надолго выбросить часть игровых объектов из обработки, не создав этим заметных багов.  И что теперь, конец всему? Дальше будем форкать Dart и переписывать его «тормозные библиотеки»? Или вообще выбросим этот язык и начнём переписывать всю программу на очередном «модном современном и точно самом-самом-быстром-в-мире» языке?  Нет. Предлагаю вместо этого просто немного поработать головой, тем более что часть решений прямо лежит на поверхности, но из-за их банальности могут просто остаться незамеченными.  Языки высокого уровня – это прекрасно. Они берут на себя кучу мелкого головняка, позволяя сконцентрироваться на главном. Но если вам приходится заниматься глубокой оптимизацией, то от части этих удобств придётся отказаться.  Вот взять, например, Iterable. Он одинаковый и для List, и для Map, и для Set – но только на уровне интерфейса. А реализации совсем не идентичны. Поэтому если у вас есть набор данных внутри HashSet или HashMap, и вам приходится много раз циклом по ним проходиться – стоит сделать один лишний проход, чтобы сохранить эти данные в обычный List. Итерация по простому списку будет самой дешевой. И да, лучше бы этот список сделать немодифицируемым (List.filled(emptyObject,growable:false)), а элементы в него добавлять через перезапись индексов в цикле for – в том случае, если код для заполнения списка у вас тоже находится в критичной для производительности зоне.   Вот вам наглядный пример из недр исходников Dart. Сравните работу итератора обычного списка и HashSet:  Лично удостовериться можно тут и тут.  В обычном списке итерация идёт по одному списку, что ожидаемо, а внутри HashSet находится множество списков, по которым итератор скачет согласно какой-то логике. Сейчас не так важно, какой именно, важно, что этой логики больше, чем просто увеличение индекса.  Схожая картина получится и в случае битвы между доступом по ключу для HashSet и доступом по индексу для List. Последний, очевидно, выдаст результат быстрее.  Внимание! Рецепт уровня «Капитан Очевидность»!  Посмотрите на ваши данные, которые вы храните в Map. Какой у них цикл жизни? В какой момент вы знаете, сколько элементов будет в коллекции? Когда вы коллекцию заполняете и когда проверяете результат? Потому что процесс можно значительно ускорить в случае, если:  Вы знаете, сколько элементов будет в коллекции ещё до начала её заполнения и использования. Сами эти классы есть возможность хранить в обычном индексированном списке, немодифицируемом по крайней мере на период работы с Map. Операций вытаскивания значения из Map достаточно много: вызов жирной полоской светится в Flame Chart. В противном случае оптимизация не имеет смысла, т.к. дополнительный обход элементов в списке сделает алгоритм только медленнее.     Если всё сходится, тогда можно оптимизироваться:  Объекты, которые раньше были ключом для Map, соберите в немодифицируемый массив List<T> Создайте немодицифируемый массив List<T> , в котором вы будете хранить ваше значение для Map. Количество элементов равно количеству объектов из предыдущего шага Ну а дальше всё совсем просто: позиция в массиве «ключей» соответствует позиции в массиве «значений», и пока оба эти массива остаются неизменяемыми, вы можете получать данные просто по индексу вместо дорогого доступа по ключу. Описание довольно абстрактное, ну, потому что кейсы тоже могут быть разными, и кто знает, какой у вас. Попытаюсь добавить конкретики примером кода, который, правда, тоже высосан из пальца, без натягивания на конкретную ситуацию это не имеет смысла. Предсказуемые итоги бенчмарка:  Микросекунды HashSet List Unmodifiable List 4619877 257136 274073 4562852 241840 238391 4154912 264096 237066 4226805 176653 267829 3920770 299337 228002 4676857 376788 268611 3132388 305172 235565 4776402 338262 264673 4009885 341700 246955 3889708 215204 271635 4032407 242154 295858 3886225 239396 235958 4123772 209347 235065 2955919 257692 255938 4483262 237181 235648 4096802,733 266797,2 252751,1333 Кажется, ради 15-кратного прироста стоит и заморочиться и откатиться со сложной структуры данных на простую, с сексуальных итераторов на дедовский «for(i=0….»  Но что если условия задачи откатиться обратно на списки не дают? Даже в этом случае у нас всё ещё есть возможность увеличить производительность.  Всё дело в хэш-функциях. Set и Map для хранения любого типа данных используют hashCode, поле, которое есть у любого объекта Dart. Но тут как с итераторами, интерфейс общий, а реализация под капотом разная. Если снова заняться гуглением, можно обнаружить в анналах истории баг репорты разработчикам языка, что реализации хэшей для похожих типов данных работают с ощутимо разной скоростью.  Вот, например, коммит, который улучшает скорость работы hashCode для целых чисел: https://github.com/dart-lang/sdk/commit/8c0df46887744e229b74bfb8c6bfc05df67f67eb . А вот до сих пор открытое обсуждение того, что Object.hashCode мог бы работать и быстрее (но это не точно): https://github.com/dart-lang/sdk/issues/24206  Из прочитанного напрашивается простой вывод: если уж мы не можем идентифицировать объект в коллекции просто по порядковому номеру, тогда хотя бы попробуем идентифицировать его просто по какому-то уникальному целочисленному идентификатору. Что получится? Смотрим очередной бенчмарк:   Микросекунды HashSetByIndex HashSet 3629561 4619877 3477259 4562852 3477259 4154912 2403903 4226805 2628835 3920770 2407924 4676857 3226889 3132388 3335361 4776402 1817651 4009885 1656588 3889708 3306465 4032407 3298499 3886225 3207514 4123772 3662507 2955919 3124403 4483262 2977374,533 4096802,73 Что ж, ну не в 15 раз, так хотя бы в 2 раза быстрее мы ещё можем наш код сделать. Всё ещё неплохой результат, всё ещё стоит того, чтобы немного заморочиться! Как говорится, если нельзя, но очень хочется – то можно!  Но здесь уже потребуются специфические структуры данных, магия побитовых операций, а также преодоление риска получения неточных результатов.  Ну прям самый простой кейс: У вас есть пары объектов Вам нужно сохранить где-то данные для каждого соотношения пар Думая о задаче так, чтобы особо думать не приходилось, быстро и просто создаём структуру данных Map<Object,Map<Object,ObjectData>> и получаем медленного тяжелого монстра. Хотя задачу он решит.  В определённых случаях, конечно, можно переписать на списки, как я описывал в позапрошлом разделе.  Ну а если на списки переписать нельзя, тогда сохраняем данные в Map<int,Data>, а целочисленный ключ, идентифицирующий пару объектов, получаем с помощью побитового И, например: final key = a.hashCode & b.hashCode Ну, это был уровень «я не волшебник, я только учусь». Но есть более специфичные ситуации, для которых могут подойти более продвинутые структуры. Здесь я бы в первую очередь отправил всех интересующихся в этот доклад, чтобы проникнуться.  Лично я для себя в нем открыл такую штуку, как BloomFilter и XorFilter, а также репозиторий https://github.com/FastFilter/ с реализацией под кучу языков... кроме Dart. Мне очень хотелось бы встроить Xor Filter, т.к. в моей задаче список фильтруемых объектов формировался на этапе загрузки игры, и долгие вычисления здесь были допустимы, а по скорости проверки данных Xor должен обгонять Bloom. Но увы, из готового для Dart нашелся только Bloom Filter, зато аж две реализации. Для экспериментов я выбрал эту: https://pub.dev/packages/dart_bloom_filter  Дальше, как всегда, бенчмарки. Но сперва стоит сделать несколько оговорок:  В данном кейсе изменения идут на, так сказать, атомарном уровне, поэтому если в коде используются неэффективные методы, то и ваши попытки оптимизации могут превратиться в тыкву. По этой же причине на небольших объёмах данных / количествах запусков / отладочных режимах разница может быть на уровне вычислительной погрешности.    Собственно, запуская бенчмарк на 10000 элементах в режиме VM я получал идентичную производительность что для HashSet, что для BloomFilter. А вот скомпилировав приложение под Windows и увеличив число элементов и итераций до 10000000 сразу стала видна разница. Классический HashSet: Странный BloomFilter:  Микросекунды HashSet   check Bloom Filter check 719610 562639 737768 607158 730092 610132 773938 558105 747718 576999 771946 570114 748582 744147 735019 659500 900041 703875 882055 675708 859248 654717 820196 700066 879067 696054 898267 726426 881244 629937 805652,7333 645038,4667 Вот, мы выжали ещё немного… на 20% быстрее и без того быстрого… стоит ли оно того для вашей задачи, с учётом сайд-эффекта – вероятности ложно-положительного результата проверки – решать вам, но хорошо, что какая-то возможность есть в принципе.  И да. Не стану вводить в заблуждение: если вы перепишете пару циклов на более оптимальный алгоритм, это может вообще никак не повлиять на скорость работы вашего приложения. А может и повлиять как-то. Зависит от того, как часто этот блок кода используется и какие вокруг него ещё есть методы, влияющие на производительность.  В моём случае итераций переписывания и последующего перепрофилирования было не менее восьми, хотя специально я их и не считал. Каждое внесённое изменение само по себе не увеличивало воспринимаемую пользователем скорость работы приложения, но убирало из Flame Chart блок с долгим кодом, либо существенно его сокращало. В итоге эффект стал ощутим только когда все оптимизации стали работать вместе. А именно эффект выражался в том, что вместо 28 FPS моя демка игрового движка стала выдавать стабильные 60, обрабатывая столкновения одновременно для 161 движущегося объекта. Ну или покажу так:  Да, BloomFilter хоть весьма мал, но всё ещё отсвечивает. Хотя HashSet.[] отсвечивал ещё сильнее. В остальном никаких «особо затяжных» циклов больше не наблюдается, как было на скринах в начале статьи, что, в целом, можно считать успехом.  У некоторых может возникнуть вопрос, а зачем так заморачиваться производительностью в одном потоке, если теоретически можно распихать вычисления по разным изолятом? Но увы, тут у Dart большие проблемы: Копировать большое количество данных между изолятами на каждый кадр – это 100% слишком накладно, а разделяемой памяти нам не завезли (разве что через костыли с ffi и проброс указателя в виде integer значения, но там мы уже уходим в разработку на C++, что не комильфо) Данные можно преобразовать в бинарный вид, например с помощью вот этой прекрасной библиотеки https://github.com/google/flatbuffers , что я и сделал в той области движка, где это было легче всего. Но при большом количестве объектов гонять сериализацию в бинарный вид всё равно накладно. Отсутствие поддержки запуска некоторых нужных библиотек внутри изолятов, наличие колбэков в переменных класса, необходимость отдельно всё переписывать для того, чтобы можно было запустить изолят в вебе – всё это добавляет поставленной задаче особой остроты и хитровыпендренности. Хотя и это я реализовал в той части фреймворка, где не потребовалось бы глубинного рефакторинга. Но так себе квест. Ваш код становится асинхронным, хотя для корректной работы логики игрового движка просто необходимо, чтобы одни вычисления завершились до того, как запустятся следующие. В итоге приходим к ситуации, когда выполнение кода от предыдущего кадра накатывает на код из следующего, и чем тяжелее вычисления, тем больше слоёв такого «набегания» получается.  По итогу больше ресурсов тратится даже не на сам ваш код, а на поддержание работы async-await, и у вас в флеймчарте будут быстро выполняющиеся функции на дарте и ооочень долго работающий код нативных библиотек платформы.  Итого, оптимизировать работу в одном потоке порою надёжнее и проще, чем раскинуть по нескольким. Вот такой вот он, наш хвалёный Dart, что уж тут поделаешь. Зато json-ы вон как парсит 😊  Код бенчмарков есть не только в спойлерах, всё можно найти в репозитории и потыкать самому: https://github.com/ASGAlex/dart_iterables_benchmarks  Важная ремарка! Каждый тест лучше запускать отдельно, т.к. если просто запустить последовательно функции в рамках одного приложения, то неизвестно в какой момент придёт Garbage Collector и испортит вам все замеры, зачищая память после предыдущего теста.  Если кому-то интересно потыкать, что там у меня за игровой движок, пожалуйте в исходники https://github.com/ASGAlex/flame_spatial_grid и в демку в вебе: https://asgalex.github.io/flame_spatial_grid/  В статье я упоминал про использование flat buffers для ускорения коммуникации с изолятом и запуск изолятов в вебе через web-workers. Вообще это отдельная тема, так что тут глубоко в неё не погружаюсь, но если кому-то интересно посмотреть примеры использования – прошу:  Схемы для flat buffers и сгенерированный код с некоторыми ручными добавками Конвертация данных в бинарный буфер, отправка в изолят (в 4 параллельных потока), получение результатов в бинарном формате с декодированием «под капотом» в читабельный  Сам изолят, адаптированный для работы в вебе в том числе Надеюсь, когда-нибудь я и о многопоточности в Dart смогу рассказать историю успеха или хотя бы частичной победы 😊   CEO/CTO, программист, энтузиаст и экспериментатор