Almaz Mubinov

Решение задачи конкурса Hola’s Spring 2015 JS Competition

14 Июл 2015 г. 

Компания Hola регулярно проводит конкурсы для программистов, которые, как правило, являются интересными и довольно сложными. В своем весеннем конкурсе Hola предлагала решить такую задачу (оригинал задачи):

В Node.js входит реализация высокопроизводительного связного списка: http://github.com/joyent/node/blob/master/lib/_linklist.js

Эта реализация написана специально для хранения idle timeouts, и не позволяет одному объекту участвовать в нескольких списках.

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

Анализ

На первый взгляд задача кажется довольно сложной, ведь ускорить вот такой код, кажется нереальным:

1
2
3
4
5
6
7
8
// remove a item from its list and place at the end.
function append(list, item) {
    remove(item);
    item._idleNext = list._idleNext;
    list._idleNext._idlePrev = item;
    item._idlePrev = list;
    list._idleNext = item;
}

// remove a item from its list and place at the end. function append(list, item) { remove(item); item._idleNext = list._idleNext; list._idleNext._idlePrev = item; item._idlePrev = list; list._idleNext = item; }

К тому же, наш код должен быть более гибким — каждый из этих объектов (list и item) должен иметь возможность участвовать в нескольких списках, а это значит, что наш код будет выглядеть примерно так:

1
2
3
4
5
6
7
function append(list, item, list_key) {
    remove(item, list_key); // remove item from list with key 'list_key'
    item._idleNext[list_key] = list._idleNext[list_key];
    list._idleNext[list_key]._idlePrev[list_key] = item;
    item._idlePrev[list_key] = list;
    list._idleNext[list_key] = item;
}

function append(list, item, list_key) { remove(item, list_key); // remove item from list with key 'list_key' item._idleNext[list_key] = list._idleNext[list_key]; list._idleNext[list_key]._idlePrev[list_key] = item; item._idlePrev[list_key] = list; list._idleNext[list_key] = item; }

Такой код в общем случае не будет быстрее чем оригинальный, так что нам нужно придумать более гибкое и быстрое решение. Судя по всему, решение задачи сводится к тому, чтобы придумать как к одному объекту привязать несколько пар параметров _idleNext и _idlePrev. При этом каждая пара должна обозначать принадлежность к своему связанному списку.

Вариантов решения несколько:
— Использовать массив item._idleNext[list_key] или item[list_key]._idleNext
— Использовать генерацию строки параметра item['_idleNext'+list_key]
— Использовать случайное имя параметра (item[random_key]), хранить отдельным списком только эти случайные имена итд.

Каждый из этих вариантов имеет узкие места, где теряется производительность.

Решение

1) Для начала оформим нашу реализацию связанного списка в виде класса. Это напрашивается, т.к. для поддержки нескольких связанных списков будет удобнее обращаться к ним как к экземплярам класса. Под «классами» имеются в виду не настоящие классы EcmaScript 6, а «классы» сгенерированные из CoffeeScript.

Получим что-то подобное (код на CoffeeScript):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class List102
 
    constructor: ()->...
 
    init: (list)->
        list.next_key = list
        list.prev_key = list
 
    # show the most idle item
    peek: (list)->...
 
    # remove the most idle item from the list
    shift: (list)->...
 
    # remove a item from its list
    remove: (item)->...
 
    # remove a item from its list and place at the end.
    append: (list, item)->...
 
    isEmpty: (list)->...
 
module.exports = List102

class List102 constructor: ()->... init: (list)-> list.next_key = list list.prev_key = list # show the most idle item peek: (list)->... # remove the most idle item from the list shift: (list)->... # remove a item from its list remove: (item)->... # remove a item from its list and place at the end. append: (list, item)->... isEmpty: (list)->... module.exports = List102

Реализация всех функций не отличается от оригинальных, кроме смены названий параметров на next_key и prev_key. Таким образом становится действительно удобно работать со связанными списками:

1
2
3
4
5
6
var List102 = require('./List102/List102');
var list1 = new List102(); // first list
var list2 = new List102(); // second list
var object = /*some object*/;
list1.init(object);
list2.init(object); // Yes, object holds another list pointers!

var List102 = require('./List102/List102'); var list1 = new List102(); // first list var list2 = new List102(); // second list var object = /*some object*/; list1.init(object); list2.init(object); // Yes, object holds another list pointers!

Однако в этом коде не решена проблема именования параметра next_key и prev_key, ведь в строках 5 и 6 этот параметр будет перезаписан и списки не будут работать корректно.

2) На самом деле решение очень простое — нужно в конструкторе класса изменять исходный код всего экземпляра класса. При этом в пределах экземпляра изменить названия  next_key и prev_key на что-то более уникальное.  Это можно сделать если каждую функцию в классе привести к строке (@[func_name]+»), изменить полученный исходный код и объявить заново функцию  (eval('this.'+func_name+'='+new_code)).

Исходный код конструктора класса List102:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
constructor: ()->;
    key = @generateKey()
    next_key = 'n'+key
    prev_key = 'p'+key
 
    # replacing code of functions in runtime =)
    for _func of @
        if typeof @[_func] == 'function' and _func != 'generateKey'
            source = @[_func]+""
            new_code = source
                .replace(/next_key/g, next_key)
                .replace(/prev_key/g, prev_key)
 
            if new_code!=source
                eval('this.'+_func+'='+new_code)

constructor: ()->; key = @generateKey() next_key = 'n'+key prev_key = 'p'+key # replacing code of functions in runtime =) for _func of @ if typeof @[_func] == 'function' and _func != 'generateKey' source = @[_func]+"" new_code = source .replace(/next_key/g, next_key) .replace(/prev_key/g, prev_key) if new_code!=source eval('this.'+_func+'='+new_code)

Таким образом при создании экземпляра класса в runtime мы заменили все next_key и prev_key в классе на какую-то случайную строку. В этом случае полученное случайное имя параметра больше не нуждается в дополнительной обработке (конкатенации, дополнительных массивах итд). Таким образом при дальнейшем обращении к экземпляру класса наш связанный список будет обращаться непосредственно к нужному параметру. Тем самым мы добиваемся производительности не хуже, чем в оригинальном варианте.

Полную версию решения можно найти в моем репозитории на github: https://github.com/mubinov/hola2015solution

Оценка производительности

В benchmark.js находится тест для оценки производительности оригинального связанного списка и моего решения. Результат оценки:

Benchmark for original linklist:
    Timer 1 (creating and appending 100k items): 972ms
    Timer 2 (shift random 10k items): 1540ms
    Timer 3 (peek random 10k items): 276ms
    Timer 4 (remove random 10k items): 413ms

Benchmark for List102:
    Timer 1 (creating and appending 100k items): *823ms*
    Timer 2 (shift random 10k items): *1456ms*
    Timer 3 (peek random 10k items): *261ms*
    Timer 4 (remove random 10k items): *392ms*


                   Full table

            original linklist     List102
Timer 1         972                 823
Timer 2         1540                1456
Timer 3         276                 261
Timer 4         413                 392

Стоит пояснить, что тест синтетический и показывает только то, насколько быстро проходят тест оба решения. В тесте на моей конфигурации сервера с небольшим перевесом побеждает List102 (мое решение), что говорит о том, что в общем случае показатели производительности не ухудшились.

Ссылки:
Задание конкурса
Финальные работы и итоги
Мое решение


Общее
Almaz Mubinov © 2010-2023