Сортировка unicode NIF работает медленнее, чем реализация Pure Erlang

Я пытаюсь оптимизировать существующую библиотеку сопоставления Юникода (написанную на Erlang), переписав ее как реализацию NIF. Основная причина в том, что сортировка требует интенсивной работы процессора.

Ссылка на реализацию: https://github.com/abhi-bit/merger

Сортировка Unicode строк 1M через очередь приоритетов на основе Pure Erlang:

erlc *.erl; ERL_LIBS="..:$ERL_LIBS" erl -noshell -s perf_couch_skew main 1000000 -s init stop
Queue size: 1000000
12321.649 ms

Сортировка Unicode 1M строк через биноминальную кучу на основе NIF:

erlc *.erl; ERL_LIBS="..:$ERL_LIBS" erl -noshell -s perf_merger main 1000000 -s init stop
Queue size: 1000000
15871.965 ms

Это необычно, я ожидал, что это будет примерно в 10 раз быстрее.

Я включил eprof/fprof но они не очень полезны, когда дело доходит до модулей NIF, вот что eprof сказал о выдающихся функциях

FUNCTION                                         CALLS      %     TIME  [uS / CALLS]
--------                                         -----    ---     ----  [----------]
merger:new/0                                         1   0.00        0  [      0.00]
merger:new/2                                         1   0.00        0  [      0.00]
merger:size/1                                   100002   0.31    19928  [      0.20]
merger:in/3                                     100000   3.29   210620  [      2.11]
erlang:put/2                                   2000000   6.63   424292  [      0.21]
merger:out/1                                    100000  14.35   918834  [      9.19]

Я уверен, что реализация NIF могла бы быть сделана быстрее, потому что у меня есть чистая реализация C юникодного сопоставления на основе двоичной кучи с использованием динамического массива, и это намного быстрее.

$ make
gcc -I/usr/local/Cellar/icu4c/55.1/include  -L/usr/local/Cellar/icu4c/55.1/lib  min_heap.c collate_json.c kway_merge.c kway_merge_test.c -o output -licui18n -licuuc -licudata
./output
Merging 1 arrays each of size 1000000
mergeKArrays took 84.626ms

Конкретные вопросы я здесь:

  • Какое замедление ожидается из-за связи Erlang <-> C в модуле NIF? В этом случае замедление, вероятно, в 30 раз или более между чистой реализацией C и NIF
  • Какие инструменты могут быть полезны для отладки замедления, связанного с NIF (как в этом случае)? Я пытался с помощью perf top чтобы увидеть вызов функции, верхние (показывали некоторые шестнадцатеричные адреса) исходили из "beam.smp".
  • Какие возможные области, на которые я должен обратить внимание при оптимизации NIF? Например: я слышал, что нужно сохранять данные, передаваемые между Erlang в C и наоборот, минимальными, есть ли еще такие области, которые стоит рассмотреть?

1 ответ

Решение

Затраты на вызов NIF крошечные. Когда среда выполнения Erlang загружает модуль, который загружает NIF, он исправляет код луча модуля инструкцией эмулятора для вызова в NIF. Сама инструкция выполняет небольшую настройку перед вызовом функции C, реализующей NIF. Это не та область, которая вызывает ваши проблемы с производительностью.

Профилирование NIF во многом аналогично профилированию любого другого кода C/C++. Судя по вашему Makefile, кажется, что вы разрабатываете этот код для OS X. На этой платформе, если у вас установлен XCode, вы можете использовать приложение Instruments с инструментом CPU Samples, чтобы увидеть, где ваш код тратит большую часть своего времени. В Linux вы можете использовать инструмент callgrind от valgrind вместе с эмулятором Erlang, созданным с поддержкой valgrind, для измерения вашего кода.

То, что вы найдете, если вы используете эти инструменты в своем коде, например, что perf_merger:main/1 проводит большую часть своего времени в merger_nif_heap_getкоторый в свою очередь проводит заметное количество времени в CollateJSON, Эта функция, кажется, вызывает convertUTF8toUChar а также createStringFromJSON немного. Ваш NIF также, кажется, выполняет много распределения памяти. Это те области, на которых вы должны сосредоточиться, чтобы ускорить ваш код.

Другие вопросы по тегам