24 юли 2011

Game of Life (javascript + html5’s canvas)

Категория: Dev,HTML5,WebLucho @ 23:55

Тези дни отново ме загриза съвестта, че не работя по някакъв сайд-проект – мой собствен или open source и за това днес реших да почета и понапиша Game of Life.

Оказа се доста прост алгоритъм с доста интересна история, която и сами може да си прочетете :-)

Демо може да видите тук: http://dailyffs.com/life/

Сорсът е тук: https://gist.github.com/1102964

Коментарът към сорса от страна на @skanev е тук: https://twitter.com/skanev/status/95213748092026880

Това, което искам да кажа в тази статия е, че колкото и да са бързи съвремените Javascript engine-и и колкото и разни светила и икони на браузър вендорите да се хвалят, че производителността скача многократно от версия до версия, това не значи че не може да забързате всичко още малко много. Цената на „забързването“ е „угрозняване“ и оптимизиране на кода. В моя конкретен случай – на една функция, която се вика 60000 пъти на преизчисление.

Въпросната фунцкия:


function getLiveNeighbours(id, data) {
  var x = id % canvas.width;
  var y = parseInt(id / canvas.width);

  var cnt = 0;
  for (var i = -1; i < 2; i++) {
    for (var j = -1; j < 2; j++) {
      if ((i != 0 || j != 0) &&
        !(x+i < 0 || x+i >= canvas.width || y+j < 0 || y+j >= canvas.height) &&
        data.data[4*((y+j)*canvas.width+(x+i))] != THE_BLACK) {
        cnt++;
      }
    }
  }
  return cnt;
}

Простата оптимизация на кода включва:

  1. Премахване на извикването на функцията (тялото на функцията се ползва директно) – спестява 60000 извиквания и времето за изпълнение на една итерация пада от 500ms на 270ms
  2. Заменяне на двата вложени цикъла проверяващи за броя на съседни клетки – от 270ms на 150ms
  3. Опростяване на пресмятанията (премахване на умножения) – от 150ms на 60ms
  4. Създаване на локални референции към често използваните данни (вместо постоянни извиквания от типа „obj.data“ при изчисленията се създава и използва локална променлива) – от 60ms на 50ms
// (4)
var width = canvas.width*4;
var d = data.data;
var nd = newdata.data;
var cw = canvas.width;
var ch = canvas.height;
var base = 0;

for (var i = 0; i < d.length/4; i++) {
  var x = i % cw;
  var y = parseInt(i / cw);

  var cnt = 0;
  // (1), (2), (3)
  if (x > 0 && d[base-4] != THE_BLACK) cnt++;
  if (x < cw-1 && d[base+4] != THE_BLACK) cnt++;
  if (x > 0 && y > 0 && d[base-4-width] != THE_BLACK) cnt++;
  if (y > 0 && d[base-width] != THE_BLACK) cnt++;
  if (x < cw-1 && y > 0 && d[base+4-width] != THE_BLACK) cnt++;
  if (x > 0 && y < ch-1 && d[base-4+width] != THE_BLACK) cnt++;
  if (y < ch-1 && d[base+width] != THE_BLACK) cnt++;
  if (x < cw-1 && y < ch-1 && d[base+4+width] != THE_BLACK) cnt++;
  ...

В крайна сметка производителността скочи 10 пъти, макар логиката на кода да изглежда по-зле от преди. Не, че преди изглеждаше много красива, но все пак :D

Разбира се има и още един доста по-умен начин за техническа оптимизация – Web Workers. Те са панацеята на всички проблеми, началото и края, давид и голиат, содом и гомор, ин и ян… за тях може да почетете тази статия - Mandelbrot + Web Workers

Забележка:

  • става дума за проста техническа оптимизация на кода, а не за подобрения на алгоритъма!
  • вероятно оптимизациите в javascript engine-ите не се фокусират върху случаи на извикване на функция 60 хиляди пъти… а би трябвало.

Тагове: , , ,