Производительность Node.js/coffeescript на алгоритме, интенсивно использующем математику

Я экспериментирую с node.js, чтобы построить некоторую логику на стороне сервера, и реализовал версию алгоритма алмазного квадрата, описанного здесь в coffeescript и Java. Учитывая все похвалы, которые я слышал о производительности node.js и V8, я надеялся, что node.js не будет слишком сильно отставать от версии java.

Однако на карте 4096x4096 Java завершается менее чем за 1 секунду, а node.js/coffeescript на моей машине занимает более 20 секунд...

Это мои полные результаты. ось x - размер сетки. Лог и линейные диаграммы:

linear

журнал

Это потому, что с моей реализацией coffeescript что-то не так, или это просто природа node.js?

Кофескрипт

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)

Ява

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 появился новый стандарт для статически типизированных массивов, который уже поддерживается некоторыми браузерами. Хотя они еще не поддерживаются в самом Node, вы можете легко добавить их с помощью библиотеки: 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

Если вы ищете производительность в таких алгоритмах, как coffee/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, когда узел его запускает.

Как и любая другая среда javascript, node является однопоточным. Двигатель 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
Да, я не думаю, что это кофескрипт против 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