Добрый вечер, Хабр. Немного отвлекусь от расчетов больших и страшных девайсов для выхода за пределы гравиколодца. Есть идея запустить небольшой скрипт, рисующий красивые визуалы (которые потом можно пустить или на пиксел-арт, или на текстуры к чему-нибудь хайтековому).

Сами клеточные автоматы - огромное поле, которое можно рассматривать с самых разных аспектов.

Можно варьировать правила, можно пытаться уйти от брутфорсного перебора вариантов к более изящному запоминанию типичных сценариев и подбором возможных вариантов развития по хэшу клеточного рисунка (Golly, например). Но моя цель - достаточно редкие правила как MirekGro и Bombers, и желания/сил возиться с адаптацией существующих hashlife-алгоритмов к этим двум своеобразным правилам нет.

Правило Bombers. Красиво ведь?
Правило Bombers. Красиво ведь?
Это - MirekGro. Оно же StarWars. И да, оно сделано на триалке
Это - MirekGro. Оно же StarWars. И да, оно сделано на триалке

А еще хочется испытать одномерные "хаотические" правила вроде Rule30 для целей, с визуалами пока вовсе не связанными. Так что пусть будет bruteforce-моделирование с пересчетом сумм вокруг каждой клеточки - просто, быстро в написании и достаточно ресурсоемко.

Как хранить состояние клетки. POJO vs bitwise record

Cостояний у КА может быть больше, чем только "жив/мертв". В MirekGro клетка может пребывать в 4-х состояниях, в Bombers - в 25-и состояниях. Для отслеживания перехода "сейчас/потом" можно или хранением состояние как plain old javascript object вида { now, next }, или же в виде бинарной записи на unsigned Int16, старший байт которого - это состояние "потом", а младший - "сейчас". Посмотрим, какой вариант лучше в производительности.

Модельная ситуация - перевести состояние "потом" в состояние "сейчас", а затем назначить новое состояние "потом". Для большей надежности повторим этот процесс несколько раз. Тестовый вариант - массив из 100000 элементов, который мы будем тасовать 500 раз.

(Не)научные эксперименты проводим в node 12.18.4 на камне i5-8600KF, которому дано 32Гб RAM.

Эксперимент 1. POJO против bitwise record
Эксперимент 1. POJO против bitwise record

Тест с бинарной записью прошел в среднем за 25,092мс. Тест с объектом - за 43,574мс. Бинарная запись обеспечивал выигрыш в ~1,74 раз. Рост не на порядок, но и почти двукратное ускорение вычисление - это приятно. Так что модель КА будет работать на двухбайтовом числе.

Расплетение массива в вектор vs квадратной матрицы

Еще один момент - как лучше хранить модельные данные, стоит ли держать их в матрице или лучше расплести матрицу в один (но очень длинный вектор)? А заодно - как различается производительности двухмерных массивов в зависимости от того, обычные ли это массивы JS или же новомодные (или уже нет) TypedArrays?

Пройдемся по каждому элементу массива и раз за разом присвоим ему несколько значений. Первый эксперимент - сравнение прогонов по матрице 1000 x 1000, "расплетенной" в вектор из 1000000 элементов. Для обычных Array пять прогонов подряд (миллион элементов, 100 перетасовок) показывают, что одномерная развертка оказалась быстрее в ~ 3,75 раз (245,8мс против 922,8мс)

Array vs Array[]
Array vs Array[]

Теперь запустим аналогичный эксперимент с Uint8Array. Тот же миллион элементов, те же 100 тасовок. Пять прогонов показали, что для Uint8Array разность в производительности между матрицей и вектором - в пределах погрешности измерения performance.now().

Uint8Array vs Uint8Array[]. Почти никакой разницы
Uint8Array vs Uint8Array[]. Почти никакой разницы

Можно предположить, что такая разница (точнее - ее полное отсутствие) связано с тем, что для обычного Array постоянно происходит перерасчет границ массива (т.к. каждый его элемент может иметь произвольный размер), что приводит к просадке производительности при работе с многомерным массивом. Для TypedArray такой проблемы быть не должно из-за фиксированного размера каждого элемента. Кстати, Uint8Array также оказался немного быстрее (где-то на 10%), чем обычный Array.

Выводы

  1. Даже в JS можно спуститься на уровень работы с отдельными байтами и получит от этого ощутимый прирост производительности. Ну или багов. И удовольствия

  2. Двухмерные массивы на обычном Array - это весьма прожорливые конструкции (а ведь я даже не пытался экспериментировать с разреженными массивами или диагональными матрицами, где ситуация может стать совсем забавной)

  3. TypedArrays обладают преимуществом в производительности, и с ними нет необходимости "расплетать" 2d-матрицу в вектор

  4. Голоса в голове просят повторить эксперимент уже не для IntArray, а для Float. Потому что это уже может пригодиться в Rocket Science. Для той же баллистики, например

Комментарии (5)


  1. nin-jin
    04.12.2021 22:07
    +1

    Есть ещё вариант со множествами из координат (по множеству на каждый цвет), это позволяет работать с квазибесконечными полями:


    1. the_stucky Автор
      04.12.2021 22:57

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

      for( let y = -1 ; y <= 1 ; ++y ) for( let x = -1 ; x <= 1 ; ++x ) { if( !x && !y ) continue
      if( prev.has( key( nx + x , ny + y ) ) ) ++sum
      }
      if( sum != 3 && ( !prev.has( nkey ) || sum !== 2 ) ) continue state.add( nkey )
      И функцию key(x, y), возвращающую состояние ячейки с произвольными координатами


      1. nin-jin
        05.12.2021 00:00

        Пересчёт происходит лишь вокруг живых клеток. А координаты упаковываются в int32, что позволяет использовать это число в качестве ключа.


  1. Saiv46
    06.12.2021 08:19

    Для большей производительности придётся использовать WebAssembly, либо шейдеры WebGL


    1. the_stucky Автор
      06.12.2021 20:25

      Если визуализация, то да. Если незатейливый number crunching - то, скорее openCL + wasm