Почему время наполнения массива отличается?

Ссылка скопирована
1 ответ

Приветствую!
Ради эксперимента написал две функции, которые выполняют одно и то же действие (наполняют массив указанного размера), но делают это по-разному. Отличие видно в коде.

П.с. на названия "старый" и "новый" не обращайте внимания, сделал исключительно для удобства.

const fillList = (size) => {     const list = new Array(size);      for (let i = 0; i < size; i++) {         list[i] = i;     }      return list; };  const fillListOld = (size) => {     const list = [];      for (let i = 0; i < size; i++) {         list.push(i);     }      return list; };  const timer = (size) => {     console.log(`Наполняем старым способом`);     const date = new Date();     fillListOld(size);     const date2 = new Date();     console.log(`Наполнили за ${date2.getTime() - date.getTime()}`);     console.log(`Наполняем новым способом`);     const date3 = new Date();     fillList(size);     const date4 = new Date();     console.log(`Наполнили за ${date4.getTime() - date3.getTime()}`); };  const size = 100000000; timer(size); timer(size); timer(size);

const fillList = (size) => { const list = new Array(size); for (let i = 0; i < size; i++) { list[i] = i; } return list; }; const fillListOld = (size) => { const list = []; for (let i = 0; i < size; i++) { list.push(i); } return list; }; const timer = (size) => { console.log(`Наполняем старым способом`); const date = new Date(); fillListOld(size); const date2 = new Date(); console.log(`Наполнили за ${date2.getTime() - date.getTime()}`); console.log(`Наполняем новым способом`); const date3 = new Date(); fillList(size); const date4 = new Date(); console.log(`Наполнили за ${date4.getTime() - date3.getTime()}`); }; const size = 100000000; timer(size); timer(size); timer(size);

Результат отработки:
Наполняем старым способом
Наполнили за 1079
Наполняем новым способом
Наполнили за 2698
Наполняем старым способом
Наполнили за 811
Наполняем новым способом
Наполнили за 2620
Наполняем старым способом
Наполнили за 890
Наполняем новым способом
Наполнили за 2659

Собственно, у меня два вопроса:
1. Почему время наполнения массива старым способом почти в 3 раза меньше, чем новым?
2. Почему при повторном запуске время наполнения старым способом уменьшилось на 200мс, а у нового вообще не произошло изменений? (полагаю, это связано с оптимизацией движком, но хочется больше деталей)

Запускал на nodejs 21.6.1

Дополнительно:

В новом способе ты создаешь здоровенный "массив с дырками". Встречал мнение, что это деоптимизирует, но почему - не совсем понятно. В любом случае, это не будет однократным выделением памяти под 100000000 целых чисел, как в случае new Int32Array(size)

  • Alexandroppolus,
    Вот да

    По новому способу
    С одной стороны я думал, что это как раз будет единократным выделением памяти, в связи с чем при наполнении он не будет перестраиваться в памяти. Однако, затем я вспомнил, что это js)

    В js'е массив - тот же объект. А объекты реализованы hash-таблицами.
    В связи с чем моя догадка заключается в том, что он дольше работает, т.к. постоянно вычисляет hash, чтобы записать значение по нужному индексу, но могу быть не прав, поэтому хотелось увидеть мнение более опытных разработчиков

    Спасибо за наводку с Typed Arrays
    Если использовать Uint32Array, то время наполнения новым способом существенно сокращается (занимает 100мс вместо +-2600)

  • DeveleinDev, это ещё и от движка зависит, но "новый" вариант в любом случае проигрывает, да.
  • Автор, ты задаешь правильные вопросы. Но у меня вопрос к условиям эксперимента.
    В тегах указано слишком много. Мы не можем проводить аналогии между браузерным JS
    и той технологией которая называется Node.JS. Они обе - хорошие в своих сегментах
    но сравнивать так нельзя.

    Когда публикуешь отчет - желательно приводить среднее время выполнения или даже лучше
    процентиль.

    Вот среднее

    > (1079 + 811 + 890) / 3 val res0: Int = 926  > (2698 + 2620 + 2659) / 3 val res1: Int = 2659

    > (1079 + 811 + 890) / 3 val res0: Int = 926 > (2698 + 2620 + 2659) / 3 val res1: Int = 2659

    Старый способ быстрее более чем в 2 раза. В чем причина - непонятно. Возможно
    сработала типизация либо боксинг. И транслятор/jit поняли что речь идет не об
    абстрактных объектах а о целых числах. Массив целых оказался компактнее.
    Почему и как это происходит - нам не ведомо.
    Надо дизассемблировать саму сборку которая получена после работы runtime
    и смотреть что там собрано и как.

  • Массивы в V8 это большой и страшный зверь с множеством оптимизаций и специальных случаев.

    Так навскидку вот статьи:
    https://v8.dev/blog/elements-kinds
    https://itnext.io/v8-deep-dives-understanding-arra...

  • mayton2019,

    Мы не можем проводить аналогии между браузерным JS
    и той технологией которая называется Node.JS. Они обе - хорошие в своих сегментах
    но сравнивать так нельзя.

    Почему? Что в хромиумах, что в ноде используется в8. Да и ничего не мешает сравнить производительность и там, и там.
    В фф да, там уже всё по другому (естественно, медленнее).

  • WbICHA, ну если мы с тобой следующим тестом будем тестировать создание файлов то вдруг
    окажется что Node и браузер это очень сильно разные вещи. Хотя да... они могут использовать
    какой-то один engine.

    Но когда в топик приходит человек с бенчмарком - то самый первый вопрос к нему это на каком
    программном продукте мы тестироуем (разрядность 32/64) и ОС и версии всего-всего.

    Поэтому я категорически не согласен с таким волюнтаризмом в тестах.

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

  • mayton2019,

    ну если мы с тобой следующим тестом будем тестировать создание файлов то вдруг
    окажется что Node и браузер это очень сильно разные вещи.

    Логично, ведь в браузере нельзя просто так взять и создать файл.)

    Но когда в топик приходит человек с бенчмарком - то самый первый вопрос к нему это на каком
    программном продукте мы тестироуем (разрядность 32/64) и ОС и версии всего-всего.

    Да, но всё же то, что это винда и то, что разрядность 64, это практически дефолт.
    Я, ради интереса, щас потестил на онлайн ноде и там результаты обратные, "новый" способ раза в 2 быстрее, чем старый. У них, понятно, линуха и зеон.
    Так что, DeveleinDev, учти, что если ты сервер пишешь, то там всё может быть сильно иначе.

  • Люблю It. Любую теорию можно развалить одним фактом.
  • Запускал на nodejs 21.6.1 в терминале
    macOS Sonoma 14.3, m2, 16 ОЗУ

    WbICHA не подскажешь, где тестировал? (я про онлайн ноду)

    Я только что прогнал на https://www.jdoodle.com/execute-nodejs-online/
    100 000 000 элементов не получилось сделать, не хватило памяти.

    Для 50 000 000 старый - 1570, новый - 4422 (старый быстрее в 3 раза)
    Для 33 600 000 старый - 1540, новый - 4108 (старый быстрее в +- 2.5 раза)
    Для 33 500 000 старый - 1482, новый - 320 (новый быстрее в 5 раз)
    Для 10 000 000 старый - 458, новый - 96 (новый быстрее в 4.5 раза)

    Где-то между 33.500.000 - 33.600.000 происходит сильный перегиб между этими способами

  • Lynn «Кофеман»
    Спасибо за статьи!
    Изучу и отпишу здесь, если найду в них ответ на свой вопрос
  • DeveleinDev,

    не подскажешь, где тестировал? (я про онлайн ноду)

    Хз, первые 3 результата в гугле. Везде серваки на линухе и на аналогичных процах, так что результаты, очевидно, везде аналогичные.

  • Ответы:

    Метод push медленнее, чем обращение к индексу на прямую. Ну и выделение памяти через new Array даёт свои бонусы. Так как ты выделил фиксированное количество

    • Если push медленнее, чем обращение к индексу напрямую, то почему с push работает быстрее, чем с обращением к индексу напрямую?)
    • DeveleinDev, а разве оно так и работает?) там дело даёт ещё new Array, что компенсирует метод push
    • Немного изменил функцию с наполнением по индексу, чтобы снять метрики детальнее
      const fillList = (size) => {      console.log(`Наполняем массив размером ${size}`);     const date = new Date();      const list = new Array(size);      const date2 = new Date();     console.log(`Создали массив за ${date2.getTime() - date.getTime()}`);     console.log(`Наполняем по индексу`);      for (let i = 0; i < size; i++) {         list[i] = i;     }      const date4 = new Date();     console.log(`Наполнили за ${date4.getTime() - date2.getTime()}`);      return list; };

      const fillList = (size) => { console.log(`Наполняем массив размером ${size}`); const date = new Date(); const list = new Array(size); const date2 = new Date(); console.log(`Создали массив за ${date2.getTime() - date.getTime()}`); console.log(`Наполняем по индексу`); for (let i = 0; i < size; i++) { list[i] = i; } const date4 = new Date(); console.log(`Наполнили за ${date4.getTime() - date2.getTime()}`); return list; };

      Результат

      Наполняем массив размером 100000000
      Создали массив за 0
      Наполняем по индексу
      Наполнили за 2536

      Так что да, для созданного таким образом массива push работает быстрее, чем наполнение по индексу)

    • DeveleinDev, c++ разработчик пытается понять JavaScript
    Нужно решить такую задачу?

    Опишите проблему, и специалист поможет с настройкой, исправлением ошибки или доработкой сайта. Подберём понятный план работ без лишней переписки.

    Заказать помощь
    Лучший ответ
    1
    Кирилл JS Ответ

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

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

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

    3. Доступ к памяти: Скорость доступа к памяти также может влиять на время наполнения массива. Если данные хранятся в разных областях памяти или в разных кэш-линиях, это может замедлить операции вставки.

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

    Пример кода на PHP для наполнения массива:

    $array = [];
    $start_time = microtime(true);
     
    for ($i = 0; $i < 1000; $i++) {
        $array[] = $i;
    }
     
    $end_time = microtime(true);
    $execution_time = $end_time - $start_time;
     
    echo "Time taken to fill the array: " . $execution_time . " seconds";

    $array = []; $start_time = microtime(true); for ($i = 0; $i < 1000; $i++) { $array[] = $i; } $end_time = microtime(true); $execution_time = $end_time - $start_time; echo "Time taken to fill the array: " . $execution_time . " seconds";

    Этот код создает массив и наполняет его значениями от 0 до 999. Вычисляется время, затраченное на эту операцию, и выводится на экран.

    Другие ответы (0)

    Пока нет других ответов. Будьте первым, кто поможет автору.

    Ответить на вопрос

    комментарий

    Ваш адрес email не будет опубликован. Обязательные поля помечены *

    Вам также может быть интересно