Сортировка 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 также, кажется, выполняет много распределения памяти. Это те области, на которых вы должны сосредоточиться, чтобы ускорить ваш код.