Производителност на Node.js / coffeescript на интензивен алгоритъм с математика

Експериментирам с node.js, за да изградя някаква логика от страна на сървъра и съм внедрил версия на алгоритъма с диамантен квадрат, описан тук в coffeescript и Java. Предвид всички похвали, които чух за node.js и V8 производителността, се надявах, че node.js няма да изостава твърде много от версията на Java.

Въпреки това на карта 4096x4096 Java завършва за под 1s, но node.js/coffeescript отнема над 20s на моята машина...

Това са моите пълни резултати. оста x е размерът на мрежата. Логистични и линейни диаграми:

линеен

дневник

Дали това е така, защото има нещо нередно с моята реализация на coffeescript, или това все още е само естеството на node.js?

Coffeescript

genHeightField = (sz) ->
    timeStart = new Date()

    DATA_SIZE = sz
    SEED = 1000.0
    data = new Array()
    iters = 0

    # warm up the arrays to tell the js engine these are dense arrays
    # seems to have neligible effect when running on node.js though
    for rows in [0...DATA_SIZE]
        data[rows] = new Array();
        for cols in [0...DATA_SIZE]
            data[rows][cols] = 0

    data[0][0] = data[0][DATA_SIZE-1] = data[DATA_SIZE-1][0] = 
      data[DATA_SIZE-1][DATA_SIZE-1] = SEED;

    h = 500.0
    sideLength = DATA_SIZE-1
    while sideLength >= 2
      halfSide = sideLength / 2

      for x in [0...DATA_SIZE-1] by sideLength
          for y in [0...DATA_SIZE-1] by sideLength
              avg = data[x][y] +
                  data[x + sideLength][y] +
                  data[x][y + sideLength] +
                  data[x + sideLength][y + sideLength]
              avg /= 4.0;

              data[x + halfSide][y + halfSide] = 
                  avg + Math.random() * (2 * h) - h;
              iters++
              #console.log "A:" + x + "," + y

      for x in [0...DATA_SIZE-1] by halfSide
        y = (x + halfSide) % sideLength
        while y < DATA_SIZE-1
          avg = 
            data[(x-halfSide+DATA_SIZE-1)%(DATA_SIZE-1)][y]
            data[(x+halfSide)%(DATA_SIZE-1)][y]
            data[x][(y+halfSide)%(DATA_SIZE-1)]
            data[x][(y-halfSide+DATA_SIZE-1)%(DATA_SIZE-1)]
          avg /= 4.0;

          avg = avg + Math.random() * (2 * h) - h;
          data[x][y] = avg;

          if x is 0
            data[DATA_SIZE-1][y] = avg;
          if y is 0  
            data[x][DATA_SIZE-1] = avg;
          #console.log "B: " + x + "," + y
          y += sideLength
          iters++
      sideLength /= 2
      h /= 2.0

    #console.log iters
    console.log (new Date() - timeStart)

genHeightField(256+1)
genHeightField(512+1)
genHeightField(1024+1)
genHeightField(2048+1)
genHeightField(4096+1)

Java

import java.util.Random;

class Gen {

    public static void main(String args[]) {
        genHeight(256+1);
        genHeight(512+1);
        genHeight(1024+1);
        genHeight(2048+1);
        genHeight(4096+1);
    }

    public static void genHeight(int sz) {
        long timeStart = System.currentTimeMillis();
        int iters = 0;

        final int DATA_SIZE = sz;
        final double SEED = 1000.0;
        double[][] data = new double[DATA_SIZE][DATA_SIZE];
        data[0][0] = data[0][DATA_SIZE-1] = data[DATA_SIZE-1][0] = 
                data[DATA_SIZE-1][DATA_SIZE-1] = SEED;

        double h = 500.0;
        Random r = new Random();
        for(int sideLength = DATA_SIZE-1;
            sideLength >= 2;
            sideLength /=2, h/= 2.0){
          int halfSide = sideLength/2;

          for(int x=0;x<DATA_SIZE-1;x+=sideLength){
            for(int y=0;y<DATA_SIZE-1;y+=sideLength){
              double avg = data[x][y] + 
                      data[x+sideLength][y] +
                      data[x][y+sideLength] + 
                      data[x+sideLength][y+sideLength];
              avg /= 4.0;

              data[x+halfSide][y+halfSide] = 
                  avg + (r.nextDouble()*2*h) - h;

              iters++;
              //System.out.println("A:" + x + "," + y);
            }
          }

          for(int x=0;x<DATA_SIZE-1;x+=halfSide){
            for(int y=(x+halfSide)%sideLength;y<DATA_SIZE-1;y+=sideLength){
              double avg = 
                    data[(x-halfSide+DATA_SIZE-1)%(DATA_SIZE-1)][y] + 
                    data[(x+halfSide)%(DATA_SIZE-1)][y] + 
                    data[x][(y+halfSide)%(DATA_SIZE-1)] + 
                    data[x][(y-halfSide+DATA_SIZE-1)%(DATA_SIZE-1)]; 
              avg /= 4.0;

              avg = avg + (r.nextDouble()*2*h) - h;
              data[x][y] = avg;

              if(x == 0)  data[DATA_SIZE-1][y] = avg;
              if(y == 0)  data[x][DATA_SIZE-1] = avg;

              iters++;
              //System.out.println("B:" + x + "," + y);
            }
          }
        }
        //System.out.print(iters +" ");
        System.out.println(System.currentTimeMillis() - timeStart);
    }

}

person matt b    schedule 12.08.2011    source източник
comment
data = new Array() и double[][] data = new double[DATA_SIZE][DATA_SIZE]; са много различни твърдения. Версията на Java е реален масив. Версията на JavaScript е хеш таблица, представяща се за масив.   -  person generalhenry    schedule 13.08.2011
comment
също имайте предвид, че Node.js е само наполовина JavaScript. За тежки изчисления можете да напишете c/c++ добавки nodejs.org/docs/v0. 4.10/api/addons.html   -  person generalhenry    schedule 13.08.2011
comment
За диапазони на coffeescript: не използвайте [0...DATA_SIZE-1], за това е [0..DATA_SIZE].   -  person Aaron Dufour    schedule 13.08.2011


Отговори (4)


Както посочиха други отговорили, масивите на JavaScript са основно затруднение в производителността за типа операции, които извършвате. Тъй като те са динамични, достъпът до елементи естествено е много по-бавен, отколкото при статичните масиви на Java.

Добрата новина е, че има нововъзникващ стандарт за статично въведени масиви в JavaScript, който вече се поддържа в някои браузъри. Въпреки че все още не се поддържат в самия възел, можете лесно да ги добавите с библиотека: https://github.com/tlrobinson/v8-typed-array

След като инсталирах typed-array чрез npm, ето моята модифицирана версия на вашия код:

{Float32Array} = require 'typed-array'

genHeightField = (sz) ->
    timeStart = new Date()
    DATA_SIZE = sz
    SEED = 1000.0
    iters = 0

    # Initialize 2D array of floats
    data = new Array(DATA_SIZE)
    for rows in [0...DATA_SIZE]
      data[rows] = new Float32Array(DATA_SIZE)
      for cols in [0...DATA_SIZE]
          data[rows][cols] = 0

    # The rest is the same...

Ключовият ред там е декларацията на data[rows].

С ред data[rows] = new Array(DATA_SIZE) (по същество еквивалентен на оригинала) получавам референтните числа:

17
75
417
1376
5461

И с реда data[rows] = new Float32Array(DATA_SIZE) получавам

19
47
215
855
3452

Така че една малка промяна намалява времето за работа с около 1/3, т.е. 50% увеличение на скоростта!

Все още не е Java, но е доста съществено подобрение. Очаквайте бъдещите версии на Node/V8 да намалят допълнително разликата в производителността.

Предупреждение: Трябва да се спомене, че нормалните JS числа са с двойна точност, т.е. 64-битови плаващи числа. По този начин използването на Float32Array ще намали прецизността, правейки това малко сравнение на ябълки и портокали — не знам каква част от подобрението на производителността е от използването на 32-битова математика и каква е от по-бърз достъп до масив. Float64Array е част от спецификацията на V8, но все още не е внедрено в v8-typed-array библиотека.)

person Trevor Burnham    schedule 13.08.2011
comment
Дълго необходимо допълнение към javascript! Впечатлен съм от ренесанса на JS, през който преминаваме. Виждам подобно ускоряване с твоето ощипване. Едно нещо обаче не разбирам е, че ако променя инициализатора за data от data = new Array(DATA_SIZE), за да използвам Float32Array, времето за изпълнение се увеличава с 3 пъти? - person matt b; 14.08.2011
comment
@matt Това ме изненада, но при по-нататъшно разследване се оказа, че увеличението на производителността е защото... нарушава кода. Опитайте да поставите console.log data[0][0] в края на функцията; ще откриете, че стойността винаги е undefined, когато data е Float32Array. (И да, странно е, че няма грешка по време на изпълнение – бих го приписал на незрялостта на библиотеката с въведени масиви.) - person Trevor Burnham; 14.08.2011
comment
Актуализация! От 0.5.5 Node.js вече поддържа въведени масиви (включително Float64Array)! Обърнете внимание, че клонът 0.5.x е нестабилен, но това означава, че е почти сигурно, че Node 0.6+ ще ги поддържа. - person Trevor Burnham; 31.08.2011

Ако търсите производителност в алгоритми като този, и кафе/js, и Java са грешните езици, които да използвате. Javascript е особено лош за проблеми като този, защото няма тип масив - масивите са просто хеш карти, където ключовете трябва да са цели числа, което очевидно няма да бъде толкова бързо, колкото истински масив. Това, което искате, е да напишете този алгоритъм на C и да го извикате от възел (вижте http://nodejs.org/docs/v0.4.10/api/addons.html). Освен ако не сте наистина добри в ръчното оптимизиране на машинния код, добрият C лесно ще изпревари всеки друг език.

person Aaron Dufour    schedule 13.08.2011
comment
Страхувах се, че това ще е предложението. Наистина ми харесва идеята просто да използвам един език за клиент и сървър, например и двата само coffeescript или и двата само java. Използването на Coffeescript заедно с малко C на сървъра премахва това предимство. Изглежда java е отговорът за мен, засега. - person matt b; 14.08.2011
comment
Ако това е важно за вас, тогава определено си струва да се отбележи, че Java не е добър избор за код от страна на клиента. Java не работи в повечето мобилни браузъри и значителна част от населението не я инсталира. В крайна сметка зависи от това какъв точно е проектът, но имайте предвид всички негови страни. - person Aaron Dufour; 14.08.2011

Забравете за Coffeescript за минута, защото това не е коренът на проблема. Този код така или иначе просто се записва в обикновен стар javascript, когато node го изпълнява.

Точно като всяка друга javascript среда, възелът е еднонишков. Двигателят V8 е адски бърз, но за определени видове приложения може да не успеете да надвишите скоростта на jvm.

Първо бих препоръчал да се опитате да коригирате алгоритъма си с диамант директно в js, преди да преминете към CS. Вижте какви видове оптимизации на скоростта можете да направите.

Всъщност и аз се интересувам от този проблем сега и ще разгледам как да направя това.

Редактиране #2 Това е второто ми презаписване с някои оптимизации, като например предварително попълване на масива от данни. Не е значително по-бърз, но кодът е малко по-чист.

var makegrid = function(size){
    size++; //increment by 1

    var grid = [];
        grid.length = size,
        gsize = size-1; //frequently used value in later calculations.

    //setup grid array
    var len = size;
    while(len--){
        grid[len] = (new Array(size+1).join(0).split('')); //creates an array of length "size" where each index === 0
    }

    //populate four corners of the grid
    grid[0][0] = grid[gsize][0] = grid[0][gsize] = grid[gsize][gsize] = corner_vals;

    var side_length = gsize;

    while(side_length >= 2){
        var half_side = Math.floor(side_length / 2);

        //generate new square values
        for(var x=0; x<gsize; x += side_length){
            for(var y=0; y<gsize; y += side_length){

                //calculate average of existing corners            
                var avg = ((grid[x][y] + grid[x+side_length][y] + grid[x][y+side_length] + grid[x+side_length][y+side_length]) / 4) + (Math.random() * (2*height_range - height_range));

                //calculate random value for avg for center point
                grid[x+half_side][y+half_side] = Math.floor(avg);

            }
        }

        //generate diamond values
        for(var x=0; x<gsize; x+= half_side){
            for(var y=(x+half_side)%side_length; y<gsize; y+= side_length){

                var avg = Math.floor( ((grid[(x-half_side+gsize)%gsize][y] + grid[(x+half_side)%gsize][y] + grid[x][(y+half_side)%gsize] + grid[x][(y-half_side+gsize)%gsize]) / 4) + (Math.random() * (2*height_range - height_range)) );

                grid[x][y] = avg;

                if( x === 0) grid[gsize][y] = avg;
                if( y === 0) grid[x][gsize] = avg;
            }
        }

        side_length /= 2;
        height_range /= 2;
    }

    return grid;
}

makegrid(256)
makegrid(512)
makegrid(1024)
makegrid(2048)
makegrid(4096)
person Geuis    schedule 12.08.2011
comment
Да, не мисля, че това е нещо като coffeescript срещу javascript, тъй като и моят CS код, и вашият JS код отнемат приблизително еднакво време на изпълнение и и двата са около 10 пъти по-бавни от версията на java, така че трябва да е JVM срещу node.js нещо? Но не съм сигурен как да се примиря с това, че V8 двигателят е адски бърз - person matt b; 13.08.2011
comment
Превеждането обратно на javascript е напълно безсмислено. Coffeescript не е нищо друго освен по-хубав синтаксис, който се компилира в javascript. Ако се притеснявате за времето, необходимо за компилиране на coffeescript (подсказка: не трябва да се притеснявате), стартирайте coffee -c <file> и стартирайте компилирания код с node. - person Aaron Dufour; 13.08.2011
comment
Превеждането му обратно в JavaScript улеснява експерта по JavaScript да открива проблеми. - person generalhenry; 14.08.2011

Винаги съм предполагал, че когато хората описват времето за изпълнение на javascript като „бързо“, те имат предвид спрямо други интерпретирани, динамични езици. Сравнение с ruby, python или smalltalk би било интересно. Сравняването на JavaScript с Java не е честно сравнение.

За да отговоря на въпроса ви, смятам, че резултатите, които виждате, са показателни за това какво можете да очаквате, сравнявайки тези два изключително различни езика.

person liammclennan    schedule 16.08.2011
comment
V8 повишава производителността, като компилира JavaScript в собствен машинен код, преди да го изпълни, вместо да изпълнява байт код или да го интерпретира. Допълнителни увеличения на производителността бяха постигнати чрез използване на техники за оптимизация като вградено кеширане. С тези функции JavaScript приложенията, работещи във V8, имат ефективна скорост, сравнима с компилиран двоичен файл (en.wikipedia.org/wiki/V8_%28JavaScript_engine%29) - person matt b; 17.08.2011
comment
това, че сте в уикипедия, не го прави вярно. - person liammclennan; 19.08.2011