Привет, Хабр!
Многие разработчики пишут код в условиях неопределенности. Перечислим их:
Недостаток требований
"В первый раз". Новая задача через исследование
Торопятся
Знают только некоторые приемы, их используют везде
Не погружаются глубже в задачу
Применяют допущения, которые работают не во всех случаях
В данной статье хочу разобрать простейшую задачу с собеседования. Уровень задачи элементарный, но как показывает практика, этот инструментарий регулярно упускается из виду.
Введение
Это не конкретно поставленная задача на решение, а упражнение "на подумать".
Предыстория:
Обычная жизненная ситуация меня натолкнула на написание этой статьи.
Сцена:
Около известного магазина припаркована грузовая машина с открытой задней крышкой багажника. Рядом стоит менеджер с листком и ручкой и зачитывает список товаров, который нужно выгрузить.
Парень называет позицию: ящик 24 банки энергетика. Водитель матерится: "Блин, где же этот ср....й ящик". Крутится внутри машины, глазами выискивает и не может найти.
В магазине, как правило, работает 2 сотрудника - один уже принимает товар на улице, а второй внутри - на кассе. Возле прилавка скопилась очередь.
Мне сразу пришла в голову IT-аналогия - менеджер в роли "приемщика", программисты в ролях продавца и водителя. И я, как юзер, смотрю на это и жду очередь, не могу быстро купить необходимое.
Архитектура есть, процессы есть, а работает медленно.
Дьявол конечно в деталях, я и решил погрузиться в вопрос - где мы, программисты, упускаем из виду то, что лежит на поверхности.
Любой код является декомпозированным списком последовательных инструкций. Разница только в том - насколько крупные или мелкие "кирпичики" разбиения.
Можно мельчить - каждая строка как пункт списка "сделай то", можно в функции/методы оборачивать, а можно и в модули или сервисы. Чтобы было проще видеть картину в целом или разбивать на частности.
Как говорит практика, в одном таком "блоке" чаще всего и возникает "затык", который может являться низкопроизводительным, неоптимальным, и влиять на поведение системы.
Данная статья посвящена поиску таких проблемных мест.
Предварительные соображения
Существует несколько формулировок задачи. Одна из них примерно такая:
Привести пример, когда циклы быстрее стрелочных функций
Как я уже говорил, это не конкретная задача, а "на подумать". Разбор данного примера, надеюсь, даст понимание читателям, как искать в своем коде точки роста производительности.
Поисковики на запрос когда циклы быстрее лямбд отвечают неоднозначно, есть какие-то попытки обсуждать этот вопрос на форумах и в статьях, но реальный ответ не радует: Все зависит от ситуации.
Это конечно не ответ на вопрос, всё-таки хочется получить какое-то более четкое понимание и знать куда смотреть.
Что вообще такое стрелочная функция или лямбда? Если не вдаваться в детали, это синтаксическая обертка, которая позволяет писать более структурированный код. Но у таких конструкций есть правила использования и собственное поведение, которое было задумано для конкретных вещей.
По факту у нас есть некоторая инкапсуляция - есть функционал, у него присутствуют:
Синтаксис
Что-то делает, выдает какой-то результат
Как-то работает под капотом
Ну а циклы - это более простая конструкция, конкретный код. Циклы типа foreach для коллекций уже обертки.
И, главное, в большинстве привычных операций (хотя есть отличия в разных языках), "стрелочки" работают с копиями.
Теперь можно вернуться к конкретной задаче - что быстрее и привести пример.
Задача
Описать конкретный пример, когда циклы работают быстрее "стрелок".
Рассуждаем так: циклы кажутся предпочтительнее, когда количество элементов операций достаточно большое. Потому что, при работе с лямбдами, такими, как map, forEach у нас будут происходить вызовы дополнительных внутренних функций + копирование данных в новые коллекции, что не всегда необходимо.
Давайте разберем такой вариант: возьмем в качестве набора данных список объектов, с которым нужно что-то сделать. Конкретные преобразования не важны, главное, что они есть и выполняются внутри итерации.
Сделаем на JavaScript (можно на TS, без разницы). Возьмем 100к элементов.
let listObjectsOne = [];
let listObjectsTwo = [];
//Набьем циклом списки
for (i = 0; i < 100000; i++) {
listObjectsOne.push({
id: i + 1,
name: "Name " + i,
comment: "Comment " + i
});
listObjectsTwo.push({
id: i + 2,
name: "Name " + i,
comment: "Comment " + i
});
}
Здесь мы просто создаем 2 практически одинаковых списка.
Просто пройдемся и прибавим +1 и +2 к id-шникам.
Уже на этом этапе возникает предположение - если мы создаем в начале задачи 2 списка одинакового размера с чуть разными данными внутри элементов, то можно далее протестировать скорость работы лямбд против обычных циклов.
И пример становится на свое место. Возьмем такую формулировку:
Даны два списка элементов
listObjectsOneиlistObjectsTwoодинакового размера 100к+ элементов. Нужно преобразовать все id-шники элементов следующим образом: все поля id объектов первого списка умножить на 2, все поля id объектов первого списка умножить на 7. И отдать списки далее для использования.
Простейшее решение - map
console.log("Выведем время для Первого случая - map");
let now = new Date().getTime();
listObjectsOne.map( x => {
x.id = x.id * 2
return x;
});
listObjectsTwo.map( x => {
x.id = x.id * 7
return x;
});
console.log (new Date().getTime() - now);
Замеряем время, и выводим в консоль сразу результаты списков и тайминг.

19 ms. Неплохо.
Теперь заметим, что по условию задачи размеры обоих листов одинаковые.
Т.е. мы проходим с помощью map 2 раза. 2*100к проходов.
А что, если сделать в 2 раза меньше?
И вот здесь мы придумываем циклы. Добавим простейший код ниже для сравнения:
console.log("Выведем время для второго случая - for")
now = new Date().getTime();
for (i = 0; i < listObjectsOne.length; i++) {
listObjectsOne[i].id = listObjectsOne[i].id * 2;
listObjectsTwo[i].id = listObjectsTwo[i].id * 7;
}
console.log (new Date().getTime() - now);
Получаем:

В 2 раза меньше итераций, копий в новую область памяти не происходит, конечный результат тот же, скорость в несколько раз выше.
Конечно, этот код можно улучшить еще, причесать, найти еще точки роста производительности, но вывод можно уже сделать: когда мы "плодим" лишнее количество итераций даже самыми популярными методами решений, то должны обращать внимание на саму задачу в первую очередь, далее смотреть на первое "исследовательское" решение и пытаться его упростить.
Привожу весь код в развороте.
Скрытый текст
let listObjectsOne = [];
let listObjectsTwo = [];
//Набьем циклом списки
for (i = 0; i < 100000; i++) {
listObjectsOne.push({
id: i + 1,
name: "Name " + i,
comment: "Comment " + i
});
listObjectsTwo.push({
id: i + 1,
name: "Name " + i,
comment: "Comment " + i
});
}
console.log("Выведем время для Первого случая - map");
let now = new Date().getTime();
listObjectsOne.map( x => {
x.id = x.id * 2
return x;
});
listObjectsTwo.map( x => {
x.id = x.id * 7
return x;
});
console.log (new Date().getTime() - now);
console.log("Выведем время для второго случая - for")
now = new Date().getTime();
for (i = 0; i < listObjectsOne.length; i++) {
listObjectsOne[i].id = listObjectsOne[i].id * 2;
listObjectsTwo[i].id = listObjectsTwo[i].id * 7;
}
console.log (new Date().getTime() - now);
А другие языки?
А там тоже самое. Быстро набросал такую же ситуацию на Kotlin.
Особый интерес вызвал кейс вызова Java Steam метода parallelStream на сравнительно большом списке в 10M элементов.
В результате общий цикл обыгрывает все парные лямбды, но проигрывает parallelStream в двух экземплярах.
В итоге самый быстрый:
val nowMiliSecondsParallelStream = Instant.now().toEpochMilli();
logger.info("Первый")
listOne.parallelStream().forEach {
(it.id * 2)
}
logger.info(" Второй")
listTwo.parallelStream().forEach {
(it.id * 2)
}
val intervalMiliSecondsParallelStream = (Instant.now().toEpochMilli() - nowMiliSecondsParallelStream)
logger.info("$intervalMiliSecondsParallelStream милисекунд на создание")
Прилагаю весь код в развороте.
Примеры на Java/Kotlin
fun experimentalMethod() : String {
val listOne : MutableList<TestingClassOne> = mutableListOf()
val listTwo: MutableList<TestingClassTwo> = mutableListOf()
for (i in 1..10_000_000) {
listOne.add(
TestingClassOne(
id = i,
name = "Name $i",
comment = "Comment $i"
)
)
listTwo.add(
TestingClassTwo(
id = i * 2,
name = "Name $i",
comment = "Comment $i",
data = "$i $i $i"
)
)
}
logger.info("Тестирование forEach из Котлин")
logger.info(" ")
val nowMiliSeconds = Instant.now().toEpochMilli();
logger.info(" Первый")
listOne.forEach {
(it.id * 2)
}
logger.info(" ВТорой")
listTwo.forEach {
(it.id * 2)
}
val intervalMiliSeconds = (Instant.now().toEpochMilli() - nowMiliSeconds)
logger.info("$intervalMiliSeconds милисекунд на создание")
logger.info(" ")
logger.info("Тестирование forEach с asSequence из Котлин")
logger.info(" ")
val nowMiliSecondsSequense = Instant.now().toEpochMilli();
logger.info(" Первый")
listOne.asSequence().forEach {
(it.id * 2)
}
logger.info(" ВТорой")
listTwo.asSequence().forEach {
(it.id * 2)
}
val intervalMiliSecondsSequence = (Instant.now().toEpochMilli() - nowMiliSecondsSequense)
logger.info("$intervalMiliSecondsSequence милисекунд на создание")
logger.info(" ")
logger.info("Тестирование стрима")
logger.info(" ")
val nowMiliSecondsStream = Instant.now().toEpochMilli();
logger.info(" Первый")
listOne.stream().forEach {
(it.id * 2)
}
logger.info(" Второй")
listTwo.stream().forEach {
(it.id * 2)
}
val intervalMiliSecondsStream = (Instant.now().toEpochMilli() - nowMiliSecondsStream)
logger.info("$intervalMiliSecondsStream милисекунд на создание")
logger.info(" ")
logger.info("Тестирование forEachIndexed")
val nowMiliSecondsFor = Instant.now().toEpochMilli();
logger.info(" Первый")
listOne.forEachIndexed{index, element ->
element.id * 2
listTwo[index].id * 2
}
val intervalMiliSecondsForResult = (Instant.now().toEpochMilli() - nowMiliSecondsFor)
logger.info("$intervalMiliSecondsForResult милисекунд на создание")
logger.info(" ")
logger.info("Тестирование for old school")
val nowMiliSecondsForOldSchool = Instant.now().toEpochMilli();
logger.info(" Первый")
for (i in 0..<listOne.size) {
listOne[i].id * 2
listTwo[i].id * 2
}
val intervalMiliSecondsForResultOldSchool = (Instant.now().toEpochMilli() - nowMiliSecondsForOldSchool)
logger.info("$intervalMiliSecondsForResultOldSchool милисекунд на создание")
logger.info(" ")
logger.info("Тестирование parallel стрима")
logger.info(" ")
val nowMiliSecondsParallelStream = Instant.now().toEpochMilli();
logger.info("Первый")
listOne.parallelStream().forEach {
(it.id * 2)
}
logger.info(" Второй")
listTwo.parallelStream().forEach {
(it.id * 2)
}
val intervalMiliSecondsParallelStream = (Instant.now().toEpochMilli() - nowMiliSecondsParallelStream)
logger.info("$intervalMiliSecondsParallelStream милисекунд на создание")
logger.info(" ")
return "Результат вычислений"
}И вывод лога
Тестирование forEach из Котлин
INFO 4316 --- [nio-8080-exec-1] c.k.demo.service.TestCircleForService : Первый
INFO 4316 --- [nio-8080-exec-1] c.k.demo.service.TestCircleForService : Второй
INFO 4316 --- [nio-8080-exec-1] c.k.demo.service.TestCircleForService : 493 милисекунд на создание
INFO 4316 --- [nio-8080-exec-1] c.k.demo.service.TestCircleForService :
INFO 4316 --- [nio-8080-exec-1] c.k.demo.service.TestCircleForService : Тестирование forEach с asSequence из Котлин
INFO 4316 --- [nio-8080-exec-1] c.k.demo.service.TestCircleForService :
INFO 4316 --- [nio-8080-exec-1] c.k.demo.service.TestCircleForService : Первый
INFO 4316 --- [nio-8080-exec-1] c.k.demo.service.TestCircleForService : Второй
INFO 4316 --- [nio-8080-exec-1] c.k.demo.service.TestCircleForService : 496 милисекунд на создание
INFO 4316 --- [nio-8080-exec-1] c.k.demo.service.TestCircleForService :
INFO 4316 --- [nio-8080-exec-1] c.k.demo.service.TestCircleForService : Тестирование стрима
INFO 4316 --- [nio-8080-exec-1] c.k.demo.service.TestCircleForService :
INFO 4316 --- [nio-8080-exec-1] c.k.demo.service.TestCircleForService : Первый
INFO 4316 --- [nio-8080-exec-1] c.k.demo.service.TestCircleForService : Второй
INFO 4316 --- [nio-8080-exec-1] c.k.demo.service.TestCircleForService : 430 милисекунд на создание
INFO 4316 --- [nio-8080-exec-1] c.k.demo.service.TestCircleForService :
INFO 4316 --- [nio-8080-exec-1] c.k.demo.service.TestCircleForService : Тестирование forEachIndexed
INFO 4316 --- [nio-8080-exec-1] c.k.demo.service.TestCircleForService : Первый
INFO 4316 --- [nio-8080-exec-1] c.k.demo.service.TestCircleForService : 383 милисекунд на создание
NFO 4316 --- [nio-8080-exec-1] c.k.demo.service.TestCircleForService :
INFO 4316 --- [nio-8080-exec-1] c.k.demo.service.TestCircleForService : Тестирование for old school
INFO 4316 --- [nio-8080-exec-1] c.k.demo.service.TestCircleForService : Первый
INFO 4316 --- [nio-8080-exec-1] c.k.demo.service.TestCircleForService : 342 милисекунд на создание
INFO 4316 --- [nio-8080-exec-1] c.k.demo.service.TestCircleForService :
INFO 4316 --- [nio-8080-exec-1] c.k.demo.service.TestCircleForService : Тестирование parallel стрима
INFO 4316 --- [nio-8080-exec-1] c.k.demo.service.TestCircleForService :
INFO 4316 --- [nio-8080-exec-1] c.k.demo.service.TestCircleForService : Первый
INFO 4316 --- [nio-8080-exec-1] c.k.demo.service.TestCircleForService : Второй
INFO 4316 --- [nio-8080-exec-1] c.k.demo.service.TestCircleForService : 131 милисекунд на создание
Обратим внимание, что на Kotlin были другие операции, умножение на 2 обоих id-шников, а не на 7. Здесь смысл в том, что внутри итераций могут быть абсолютно любые преобразования, функции, еще что-то. Это те самые O(n), о которых принято говорить.
2 цикла в виде foreach или map в примерах на JS - это O(2n), а в общем старом цикле O(n). В параллельных даже не буду указывать, потому что быстрее, это хорошо, но дорого по потокам.
А если через БД?
В своей предыдущей статье я писал, что многие разработчики недооценивают мощь баз данных как инструмента. Всем хочется писать только код в одном месте (с минимумом взаимодействия с БД). Давайте посмотрим на эту задачу с другой стороны.
В микросервисной архитектуре популярен REST. Возьмем его.
Если к нам на эндпоинт приходят данные в виде списков объектов, то задача при росте количества элементов становится трудоемкой. Со "своими" данными иногда лучше работать через базу.
В рамках данного разбора что мы имеем - списки примерно такого содержания на выходе:
[{id: 4, name: 'Name 0', comment: 'Comment 0'},
{id: 8, name: 'Name 1', comment: 'Comment 1'},
....
]
и
[
{id: 49, name: 'Name 0', comment: 'Comment 0'},
{id: 98, name: 'Name 1', comment: 'Comment 1'},
....
]
Прямой запрос к БД именно для конкретной задачи никаких преимуществ не дает, сами данные настолько большие, что их необходимо "перелить" из базы в сервис.
Здесь мы упираемся в сам факт соединения и получения данных.
2026-01-18T10:10:46.919+03:00 INFO 11652 --- [nio-8080-exec-1] c.k.demo.service.TestCircleForService : Первый
Hibernate:
SELECT generate_series(1, 1000000) * 7 * 7 as num,
CONCAT('Name ', cast (generate_series(0, 1000000 - 1) as text)) as name,
CONCAT('Comment ', cast (generate_series(0, 1000000 - 1) as text)) as comment
2026-01-18T10:11:00.050+03:00 INFO 11652 --- [nio-8080-exec-1] c.k.demo.service.TestCircleForService : Второй
Hibernate:
SELECT generate_series(1, 1000000) * 7 * 7 as num,
CONCAT('Name ', cast (generate_series(0, 1000000 - 1) as text)) as name,
CONCAT('Comment ', cast (generate_series(0, 1000000 - 1) as text)) as comment
2026-01-18T10:11:13.164+03:00 INFO 11652 --- [nio-8080-exec-1] c.k.demo.service.TestCircleForService : 26245 милисекунд на создание
2026-01-18T10:11:13.165+03:00 INFO 11652 --- [nio-8080-exec-1] c.k.demo.service.TestCircleForService :
2026-01-18T10:11:13.165+03:00 INFO 11652 --- [nio-8080-exec-1] c.k.demo.service.TestCircleForService : 27524
А на уровне самой базы (выборка через Dbeaver), результат естественно нормальный. Но учитываем, что там количество строк имеет естественное ограничение. При огромном числе нас будет ждать JdbcDriverError (намек на пагинацию).

Т.е., каждый раз будет возникать вопрос - что дороже, платить за вычисления в сервисе со своими данными, или брать из БД нужный преобразованный набор, но платить стоимостью коннекта и прочее.
Мое мнение - если подготовка данных (фильтрация, агрегация) имеет значительный эффект именно в БД (что происходит чаще), то лучше всегда сделать эти преобразования там, чем "забирать всё", а потом разгребать. Иными словами, если выборка из БД прямо обязательна, то мы все равно так или иначе платим за соединение, так что лучше преобразовать именно там + проверить скорость.
Что получили
Использование той или иной фичи/функциональности лежит на плечах разработчика.
Если у него не хватает знаний в конкретной области, то он будет использовать другие, не совсем подходящие под задачу решения.
Я наблюдал огромное количество ситуативных применений стрелочных функций во frontend'ах, да и в бэках, когда программист просто копипастит свои часто используемые приемы то тут, то там.
Быстро преобразовать данные через map, filtes, foreach и др., и подставить в нужное место всегда выгодно с точки зрения компактности и аккуратности кода.
Проблемы возникают, когда коллекция становится больше некоторого значения по количеству элементов.
Параллельность и работа с данными извне также имеют свою стоимость.
Как применять эти результаты
Рецепты по оптимизации кода достаточно обширны и сильно зависят от конкретных ситуаций. Поэтому хочу написать общие соображения:
Стрелочные функции или лямбды хороши для коллекций небольших размеров. Преимущества - всегда под рукой и достаточно быстры, меньше кода.
Циклы. Недостатки - чуть больше кода. Преимущества: лучше читабельность, можно объединить общие итерации, когда это удобно, устранив дополнительные проходы.
Как оптимизировать?
Использовать переменные, если есть повторные вызовы стрелок, которые делают одно и тоже. НЕ ИСПОЛЬЗОВАТЬ дополнительные переменные, если есть отличия. Лучше несколько лямбд, которые делают немного разное, чем объединять всё в одну кучу и потом использовать if-else логику.
Если размер коллекции превышает 100к - 1M, то лучше сразу посмотреть на логику выше уровнем, скорее всего там что-то не так.
Использовать циклы и параллельные вычисления по необходимости работы именно с большими коллекциями, но не злоупотреблять.
Выбирать правильную структуру данных для конкретных вещей. Чем лучше выбрана структура, тем меньше циклов и стрелочек придется писать.
Не копипастить чужой код
Оценивать производительность через исследование.
Изучать больше документацию.
И, конечно, код-ревью.
За скорость кодирования и красоту кода мы часто расплачиваемся ресурсами, снижением производительности и временем на исправление.
А что в итоге с водителем?
Он выбрал неправильную структуру организации товаров в своем багажнике. Хаотично разбросал ящики с продуктами, то тут, то там. Поэтому пришлось проходиться циклом "глазами" по всему объему. А если бы у него стояли нумерованные стопки с ящиками напитков, то взял бы и отдал сразу нужный.
Комментарии (39)

AlexeyP2026
18.01.2026 08:50дилетантский вопрос, разве лямбда функции (map, filtes, foreach) не содержат внутри себя циклы? почему они быстрее?

kostoms
18.01.2026 08:50Почему вы решили, что они быстрее? Автор же об этом и пишет: он заменил два последовательных цикла одним и получил прирост скорости, и так прямо русским по белому и написал: "В результате общий цикл обыгрывает все парные лямбды".

MikhailB7
18.01.2026 08:50Т.е. если говорить об изначальном вопросе (что быстрее циклы или стрелки), то условия эксперимента не корректные. В первом случае автор прогнал 2 последовательных цикла, во втором-один. Вместо чистого эксперимента сделана оптимизация кода и сравнение с оригинальной версией.

kostoms
18.01.2026 08:50Я так понимаю, что его идея в том, что с мапом циклы не объединишь. Но со временем там что-то сразу совсем не так - непонятно откуда в первом случае взялись 21мс, взяты слишком маленькие массивы и походу в JS есть и другие факторы, вот покрутил "первый случай" в цикле (массивы увеличены до 1М элементов):
% node loop-speed-test.js Выведем время для Первого случая - map 31 Выведем время для Первого случая - map 36 Выведем время для Первого случая - map 36 Выведем время для Первого случая - map 98 Выведем время для Первого случая - map 596 Выведем время для Первого случая - map 27 Выведем время для Первого случая - map 34 Выведем время для Первого случая - map 35 Выведем время для Первого случая - map 32 Выведем время для Первого случая - map 30 Выведем время для Первого случая - map 35 Выведем время для Первого случая - map 332 Выведем время для Первого случая - map 548Ничо так разбросец.

MikhailB7
18.01.2026 08:50Я писал безотносительно размера массивов и способов замера времени.
Сравнивать быстродействие надо на одинаковых сценариях. А то, что при использовании массива можно сценарий оптимизировать- ну этот сценарий можно, другой нельзя будет.

kostoms
18.01.2026 08:50Сценарий как раз одинаковый - множим элементы в массивах одной размерности. А выводы некорректные (хотя случайно и правильные). Кстати, добил код автора чтобы он 20 раз прокрутил цикл с лямбдами, одинарным циклом и двумя последовательными. Девиации в продолжительности с лямбдами составили от 25 до 588, для одинарного цикла - от 7 до 14, для двойного цикла - от 8 до 16. Лямбды явно медленнее да ещё их время исполнения дико варьирует.

MikhailB7
18.01.2026 08:50Решаемая задача одна. Сценарий у автора как раз разный.
Одинаковый сценарий был бы например заполнение данных в 1 массиве через лямбду и через цикл.

mixsture
18.01.2026 08:50они и не могут быть быстрее, т.к. map/filter/foreach будет вызывать функцию. То, что она безымянная и объявлена хитрым сокращенным образом ничего не меняет.
А раз это вызов функции, то должны присутствовать ассемблерные перелести подготовки (упаковка аргументов в соответствующие регистры процессора, сдвиг позиции в стеке, создание области видимости переменных), сам прыжок на функцию (который может быть совсем не одним переходом), выполнение полезного кода, затем прыжок обратно.Тогда как в цикле присутствует только выполнение полезного кода.
В компилируемых языках можно заставить компилятор включить тексты функций в места их вызовов (inline оптимизация). В случае с jit-компиляторами у меня сомнения, что удастся им это объяснить. А ждать от интерпретируемых языков такого вообще не стоит.

vadimr
18.01.2026 08:50В интерпретируемом языке map/filter/foreach – это один вызов интерпретатора, у которого дальнейший цикл крутится уже внутри его собственного скомпилированного кода; в то время как оператор цикла – это вызовы интерпретатора на каждом шаге цикла. Поэтому скорее можно ожидать, что для интерпретатора функции высшего порядка при прочих равных условиях (а не так, как у автора) будут эффективнее цикла.
А для оптимизирующего компилятора это одно и то же.

vadimr
18.01.2026 08:50(define n 10000000) (define v (make-vector n 1)) (define l (vector->list v)) (define (loop-mode) (let ((v1 (make-vector n))) (do ((i 0 (+ i 1))) ((>= i n) v1) (vector-set! v1 i (cons (* 2 (vector-ref v i)) (* 4 (vector-ref v i))))))) (define (map-mode) (map (lambda (x) (cons (* x 2) (* x 4))) l))(begin (time (loop-mode)) #!void) (time (loop-mode)) 1.450953 secs real time 1.449960 secs cpu time (1.209880 user, 0.240080 system) 1 collection accounting for 0.382245 secs real time (0.147441 user, 0.234461 system) 1039997608 bytes allocated 526 minor faults no major faults(begin (time (map-mode)) #!void) (time (map-mode)) 0.941165 secs real time 0.940383 secs cpu time (0.788342 user, 0.152041 system) 2 collections accounting for 0.527198 secs real time (0.382315 user, 0.144481 system) 1954345472 bytes allocated 3211 minor faults no major faultsЭто в режиме интерпретации. При компиляции, конечно, loop-mode выигрывает:
(time (loop-mode)) 0.146607 secs real time 0.146481 secs cpu time (0.127825 user, 0.018656 system) 1 collection accounting for 0.085572 secs real time (0.080117 user, 0.005399 system) 560000112 bytes allocated 29164 minor faults no major faults(time (map-mode)) 0.426168 secs real time 0.425797 secs cpu time (0.384492 user, 0.041305 system) 2 collections accounting for 0.281391 secs real time (0.270081 user, 0.011088 system) 1574838304 bytes allocated 85327 minor faults no major faultsЗамечания:
Здесь результаты приведены для процессора Apple M4 Pro. В архитектуре Intel, вероятно, работа с массивом будет иметь дополнительное преимущество против списка (что, однако, не имеет прямого отношения к теме обсуждения).
Я использовал интерпретатор и компилятор языка Gambit Scheme, так как для Javascript мне не известен эффективный компилятор.
Поскольку цикл эффективно работает только с массивом, а функция map требует список, то для двух вариантов пришлось применить разные структуры данных.

mixsture
18.01.2026 08:50Да, скорее всего вы правы. Учитывая, что у нас тут реальная нагрузка состоит из очень простой арифметической операции, то конструкция цикла, реализованная на низкоуровневом языке (внутри которой должны быть арифметические операции, сравнение и прыжок) может и существенно ускорить относительно ее исполнения чистым интерпретированием.

viordash
18.01.2026 08:50а разве
console.log(у первого случая бесплатный? Ведь он тоже в учет времени попал
ShapitoS999 Автор
18.01.2026 08:50Конечно, не бесплатный. Но он мал, по сравнению с остальными действиями, т.е. не сильно влияет на выводы в статье. Действительно он немного съедал. В коде поправил

rock
18.01.2026 08:50Это сильно.
Не объявлять переменные (тем самым, мутировать глобальный объект / вытаскивать значения из него - у нас же не строгий код).
Сравнивать разные операции - мутацию объектов из оригинального массива в
forи то же самое плюс создание новых массивов из них в.map, да ещё и с выводом в консоль новых массивов, вместо.forEach.Мерить производительность в консоли, не учитывать разные компиляторы для разных уровней оптимизации движка.
Мерить производительность только в одном браузере - при том, что в каждом из браузерных движков это реализовано по своему.
Использовать
new Date().getTime()(даже неDate.now()!) для измерения отрезка порядка 2мс - есть жеconsole.time/performance.now.

venanen
18.01.2026 08:50И все этого для того, чтобы сделать вывод, что два запуска map по N элементов (т.е. 2N итераций) дольше, чем один for по всем (то есть N операций). Это действительно сильный вывод, надо это всегда держать в уме, это ведь на весь мир проецируется. Например, два ведра воды, неожиданно, тяжелее, чем одно ведро. Думаю развернуть это на серию статей...

TimurZhoraev
18.01.2026 08:50на самом деле это довольно хороший камень в огород разработчиков компиляторов и языков. По-идее высокоуровневая оптимизация должна преобразовать арифметические лямбды в инлайны, также указать на более сложные что имеется стек помимо всего прочего. Плюс скрытая память для мутабельных объектов, включая списки и итерация по последовательной или произвольной индексации / хеши для множеств. Вообщем эта проблема обратной связи от компилятора-интерпретатора к пользователю на конструкциях верхнего уровня. Помимо error/warning/remark должен быть долгожданный proposition и вскрытие внутренней структуры объектов вместо голого байткода. Тогда и консоль будет условно бесплатная в виде счётчика или массива вместо вызовов системных функций. Вот все эти tricks and tips и нужно скармливать как можно скорее ИИ чтобы это не держать в голове.

plFlok
18.01.2026 08:50Уберите камень из огорода, компиляторы этим занимаются. Но не всегда это нужно.
Хоть js и не компилирует, авторы js могут отправить ноду улучшать код. Но я сомневаюсь, что вы будете рады ждать двухчасовую оптимизацию кода страницы, чтоб он выполнял цикл на 200 наносекунд быстрее. А анализ возможности оптимизаций - увы, довольно сложная работа в вычислительном плане.

MountainGoat
18.01.2026 08:50JavaScript, напомню – это чтобы кнопочка синим светилась, когда товара в корзине нет, и зелёным – когда есть.
У языков, предназначенных для написания большого кода, всё отлично инлайнится.

TimurZhoraev
18.01.2026 08:50Исторически, JS - это плагин для веб-поведения который прикрутили к голому HTML. Хотя насколько помню были попытки сразу сделать возможность передачи двоичной разметки с байткодом реализующим и CSS и JS и Cookies и HTTPS и многое другое вместе взятое. Но так как под Netscape Navigator уже тогда начали появляться аддоны + с лёгкой руки Adobe и Sun Microsystems то W3C застолбили по инерции текстовый режим ещё и с парными тегами, усложнив при этом загрузку страницы с отсутствующими элементами, хотя вполне возможно было создать WebASM, провести двоичную унификацию или что-то подобное уже тогда. Поэтому, на Big Red Button тратится больше тактов чем для решения дифур схемы с десятком транзисторов в MicroCAP (эх, хороший был симулятор). Рассматривать производительность в браузере в этом случае это всё равно что помогать комитету добивать эту тему до логического завершения в плане стандартизации зоопарка исторически сложившихся протоколов. А что там будет под катом JVM/LLVM или даже аппаратные интерпретаторы из разряда Java на ПЛИС это уже вопрос десятый, важна поддержка GPU, нативной многопоточки, взаимосвязи между ними (вкладками), управление песочницей и пользовательскими данными. Браузер по сложности уже почти миниатюрная ОС и виртуальная машина, зачем делать ОС в ОС, когда проще использовать уже готовую ОС и её инфраструктуру, заточенную нативным образом под Web без промежуточных костылей, на уровне POSIX, но это уже другой уровень .

Artyomcool
18.01.2026 08:50Чтобы не попадать в ситуацию, когда сравнивается тёплое с мягким, можно использовать любой вменяемый профайлер. Если мы говорим про JVM-based языки, то async-profiler. Посмотрите что именно у вас занимает время, и всё встанет на свои места. Окажется, что по понятным причинам, цикл, написанный корректно, абсолютно всегда быстрее лямбд (как минимум в указанных языках). Единственное возможное исключение: когда JIT смог соптимизировать лямбды до полностью эквивалентного состояния, но даже в этом случае, это зачастую хрупкое и шаткое состояние, любой чих (даже далеко от предполагаемого места) может его сломать.

TimurZhoraev
18.01.2026 08:50Там должна быть песочница с чистым холодным процессорным временем и гарантией того что это влезет в его кэш. Если JVM разрастается вне кэша то на этой границе производительность может ложиться в разы, это что касается как раз этих мелких внутренних циклов/лямбд. Произвольный доступ вместо последовательного для любой блочной памяти или кэша это катастрофа а виртуальная машина своим жиром делает именно так. Поэтому такие тесты обязательно прогонять на N от 0 до миллиона чтобы посмотреть что творится "внизу" ступеньки. Отсюда и горячие точки подлежащие оптимизации. Плюс многопроцессорные различные мьютексы и потокобезопасность тоже может клацать когда речь идёт о консолях и прочим I/O со 100500 параллель открытых вкладок.

Artyomcool
18.01.2026 08:50Простите, у вас тоже как-то всё в кучу.
песочница с чистым холодным процессорным временем и гарантией того что это влезет в его кэш
Это - что? И в какой кэш? В JVM используется много разных интересных оптимизаций, улучшающих как локальность данных, так и локальность кода. Плюс в продакшене у вас будет не песочница, а реальность, и интерференции с реальностью всё сильно меняют. Пример: моно/дуо/мегаморфизм, из-за которых JIT в бенчмарках показывает такой же результирующий код с лямбдами, как без них, но в реальности часто всё сыпется. Поэтому к измерениях в песочницах нужно относиться с соответствующим скепсисом, а мерить нужно не только CPU-time, но и Wall-time тоже, артефакты бывают разными.
Произвольный доступ вместо последовательного для любой блочной памяти или кэша это катастрофа а виртуальная машина своим жиром делает именно так.
Ещё раз к локальности данных. Как раз перемещающий GC (т.е. любой современный GC) в среднем по больнице обеспечивает прекрасную локальность по данным, они чаще всего физически лежат рядом. Исключение: плохо сконфигуринованные low-latency GC, они часто вымывают кэши постоянным сканом памяти. Если на это нужны гарантии - этого тоже можно добиться для экстремальных случаев, работая с памятью напрямую (массивы и/или off-heap решения, коих тоже минимум два в стандартной библиотеке). Работая просто с массивами, когда данных прям много, мне удавалось доходить до того, что узким местом являлось, например, предсказание ветвлений (не из-за JVM, а из-за обычного пользовательского кода).
тесты обязательно прогонять на N от 0 до миллиона чтобы посмотреть что творится "внизу" ступеньки
Тесты нужно не просто прогонять на 0 ... (очень много). Нюансов вагон и тележка. Тестировать перформанс вообще очень сложно, но начать можно и даже с wall-clock. Лучше тестировать плохо, чем не тестировать вообще. Но да, для Java есть state of the art решения, вроде JMH, которые большУю часть головной боли берут на себя.
С другой стороны, профилировать намного проще. Потому что только в реальном кейсе можно понять, а что вообще вам нужно. Ускорить холодный старт? Обеспечить высокую среднюю производительность? Гарантировать отсутствие деградации в плохих случаях? И так далее. Затем, насмотревшись на профиля, придет и некоторое базовое понимание "законов перформанса" для конкретно ваших случаев. Ну и ещё раз про проще: при профилировании "кнопочка нажаль картинка получиль", думать почти не нужно, всё видно, и всё реально, а не фантомные боли неправильно сконфигурированной тестовой системы.

TimurZhoraev
18.01.2026 08:50Вообще говоря можно обойтись без тестирования производительности на заданной платформе (не путать с тестированием надёжности и ресурсов), если имеются эвристики и модели поведения. Сама программа не такая уж и замороченная чтобы не сгенерировать теоретические оценки для идеального случая, т. к. известно сколько потребляет та или иная операция вне зависимости компилируемая она или интерпретируемая, особенно в однопоточке. Алгоритм оценивается как отметка по пятибальной шкале грубо говоря O(1), O(n),
, O(n log n) и всё в таком духе, умножение на константу в этом случае (как и в примере выше) уже считается "тонкой доводкой" под конкретную реализацию. Если эта зависимость ломается без изменения алгоритма на каком-то N или испытывает несколько "перегибов", значит с огромной долей вероятности вмешиваются внешние факторы ну или сама задача нелинейная а значит с масштабируемостью там будет много вопросов. Тестирование кеша и предсказаний в какой то мере можно обеспечить средствами из разряда Meltdown и Spectre, может даже использовать их для обратной связи мониторя что влезает а что нет. Но опять-таки там на практике всё ограничивается объёмом в пару мегабайт. Поэтому локальность данных и кода это всё-таки довольно ощутимая вещь при разработке того что должно уйти в реалтайм и эти точки "перегиба" могут оказать решающее значение если это множество экземпляров например для микросервисов. И если тут отсутствует какая-либо языковая, библиотечная или IDE поддержка с точки зрения профилирования этого дела, то собственно никакого прогресса в этом деле не обнаруживается и получается парадокс, синтаксический сахар вроде современный, а методы и подходы к дебагу/профайлингу из начала 90-х.

WhiteBehemoth
18.01.2026 08:50Когда задают такой вопрос на собеседовании, это не на "подумать", это на понимание того, что у вас обозначено "Как-то работает под капотом". И правильный ответ (вне зависимости, как он сформулирован) - это "всё зависит от реализации IEnumerable в конкретной коллекции".

Artyomcool
18.01.2026 08:50Ну вообще-то нет. Это только самый верхний слой вопроса. Дальше неплохо понимать, во что этот код переваривается после JIT. Дальше было бы неплохо понимать, а как с данными, расположенными таким образом, работает процессор и кэши. Вообще неплохо было бы понимать, что не O-нотацией единой живёт производительность, и что то, что написано в исходниках стандартной библиотеки зачастую совсем не то, что будет реально исполняться (интринсики, несколько уровней JIT и т.д.).
YegorP
Учитывая тему поста, я бы не стал мешать
forEachв одну кучу сmapиfilter. На вопрос "что быстрее: циклы vs стрелочные функции" без уточнений я бы с циклами сравнивал толькоforEach.А ещё в JS не так давно
mapи прочее завезли на итераторах: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Iterator/map (наконец-то). Это к вопросу о выделении памяти.