Ответ 1
TL; DR: Причина: вызовы RTS и STG. Решение: не вызывайте тривиальные функции Haskell из C.
В чем причина этого??
Отказ от ответственности: я никогда не называл Haskell от C. Я знаком с C и Haskell, но я редко переплетаю оба, если только я не пишу обертку. Теперь, когда я потерял доверие, позвольте начать это приключение бенчмаркинга, разборки и других отличных ужасов.
Бенчмаркинг с помощью gprof
Один простой способ проверить, что есть все ваши ресурсы, - использовать gprof. Мы немного изменим вашу строку компиляции, чтобы -pg
использовался как компилятором, так и компоновщиком (примечание: я переименовал test.c в main.c и test2.c на test.c):
$ ghc -no-hs-main -O2 -optc-O3 -optc-pg -optl-pg -fforce-recomp \
main.c Test.hs -o test
$ ./test
$ gprof ./test
Это дает нам следующий (плоский) профиль:
Flat profile: Each sample counts as 0.01 seconds. % cumulative self self total time seconds seconds calls Ts/call Ts/call name 16.85 2.15 2.15 scheduleWaitThread 11.78 3.65 1.50 createStrictIOThread 7.66 4.62 0.98 createThread 6.68 5.47 0.85 allocate 5.66 6.19 0.72 traverseWeakPtrList 5.34 6.87 0.68 isAlive 4.12 7.40 0.53 newBoundTask 3.06 7.79 0.39 stg_ap_p_fast 2.36 8.09 0.30 stg_ap_v_info 1.96 8.34 0.25 stg_ap_0_fast 1.85 8.57 0.24 rts_checkSchedStatus 1.81 8.80 0.23 stg_PAP_apply 1.73 9.02 0.22 rts_apply 1.73 9.24 0.22 stg_enter_info 1.65 9.45 0.21 stg_stop_thread_info 1.61 9.66 0.21 test 1.49 9.85 0.19 stg_returnToStackTop 1.49 10.04 0.19 move_STACK 1.49 10.23 0.19 stg_ap_v_fast 1.41 10.41 0.18 rts_lock 1.18 10.56 0.15 boundTaskExiting 1.10 10.70 0.14 StgRun 0.98 10.82 0.13 rts_evalIO 0.94 10.94 0.12 stg_upd_frame_info 0.79 11.04 0.10 blockedThrowTo 0.67 11.13 0.09 StgReturn 0.63 11.21 0.08 createIOThread 0.63 11.29 0.08 stg_bh_upd_frame_info 0.63 11.37 0.08 c5KU_info 0.55 11.44 0.07 stg_stk_save_n 0.51 11.50 0.07 threadPaused 0.47 11.56 0.06 dirty_TSO 0.47 11.62 0.06 ghczmprim_GHCziCString_unpackCStringzh_info 0.47 11.68 0.06 stopHeapProfTimer 0.39 11.73 0.05 stg_threadFinished 0.39 11.78 0.05 allocGroup 0.39 11.83 0.05 base_GHCziTopHandler_runNonIO1_info 0.39 11.88 0.05 stg_catchzh 0.35 11.93 0.05 freeMyTask 0.35 11.97 0.05 rts_eval_ 0.31 12.01 0.04 awakenBlockedExceptionQueue 0.31 12.05 0.04 stg_ap_2_upd_info 0.27 12.09 0.04 s5q4_info 0.24 12.12 0.03 markStableTables 0.24 12.15 0.03 rts_getSchedStatus 0.24 12.18 0.03 s5q3_info 0.24 12.21 0.03 scavenge_stack 0.24 12.24 0.03 stg_ap_7_upd_info 0.24 12.27 0.03 stg_ap_n_fast 0.24 12.30 0.03 stg_gc_noregs 0.20 12.32 0.03 base_GHCziTopHandler_runIO1_info 0.20 12.35 0.03 stat_exit 0.16 12.37 0.02 GarbageCollect 0.16 12.39 0.02 dirty_STACK 0.16 12.41 0.02 freeGcThreads 0.16 12.43 0.02 rts_mkString 0.16 12.45 0.02 scavenge_capability_mut_lists 0.16 12.47 0.02 startProfTimer 0.16 12.49 0.02 stg_PAP_info 0.16 12.51 0.02 stg_ap_stk_p 0.16 12.53 0.02 stg_catch_info 0.16 12.55 0.02 stg_killMyself 0.16 12.57 0.02 stg_marked_upd_frame_info 0.12 12.58 0.02 interruptAllCapabilities 0.12 12.60 0.02 scheduleThreadOn 0.12 12.61 0.02 waitForReturnCapability 0.08 12.62 0.01 exitStorage 0.08 12.63 0.01 freeWSDeque 0.08 12.64 0.01 gcStableTables 0.08 12.65 0.01 resetTerminalSettings 0.08 12.66 0.01 resizeNurseriesEach 0.08 12.67 0.01 scavenge_loop 0.08 12.68 0.01 split_free_block 0.08 12.69 0.01 startHeapProfTimer 0.08 12.70 0.01 stg_MVAR_TSO_QUEUE_info 0.08 12.71 0.01 stg_forceIO_info 0.08 12.72 0.01 zero_static_object_list 0.04 12.73 0.01 frame_dummy 0.04 12.73 0.01 rts_evalLazyIO_ 0.00 12.73 0.00 1 0.00 0.00 stginit_export_Test_zdfstableZZC0ZZCmainZZCTestZZCtest
Woah, что куча вызываемых функций. Как это сравнить с вашей версией C?
$ ghc -no-hs-main -O2 -optc-O3 -optc-pg -optl-pg -fforce-recomp \
main.c TestDummy.hs test.c -o test_c
$ ./test_c
$ gprof ./test_c
Flat profile: Each sample counts as 0.01 seconds. % cumulative self self total time seconds seconds calls Ts/call Ts/call name 75.00 0.05 0.05 test 25.00 0.06 0.02 frame_dummy
Это лот проще. Но почему?
Что происходит за?
Возможно, вы задавались вопросом, почему test
появился даже в предыдущем профиле. Ну, gprof сам добавляет некоторые накладные расходы, как видно из objdump
:
$ objdump -D ./test_c | grep -A5 "<test>:"
0000000000405630 <test>:
405630: 55 push %rbp
405631: 48 89 e5 mov %rsp,%rbp
405634: e8 f7 d4 ff ff callq 402b30 <[email protected]>
405639: 5d pop %rbp
40563a: c3 retq
Вызов mcount
добавляется gcc. Поэтому для следующей части вы хотите удалить опции -pg
. Пусть сначала проверьте разобранную процедуру test
в C:
$ ghc -no-hs-main -O2 -optc-O3 -fforce-recomp \
main.c TestDummy.hs test.c -o test_c
$ objdump -D ./test_c | grep -A2 "<test>:"
0000000000405510 <test>:
405510: f3 c3 repz retq
repz retq
на самом деле некоторая магия оптимизации, но в этом случае вы можете думать о ней как о возврате no-op (в основном).
Как это сравнить с версией Haskell?
$ ghc -no-hs-main -O2 -optc-O3 -fforce-recomp \
main.c Test.hs -o test_hs
$ objdump -D ./Test.o | grep -A18 "<test>:"
0000000000405520 <test>:
405520: 48 83 ec 18 sub $0x18,%rsp
405524: e8 f7 3a 05 00 callq 459020 <rts_lock>
405529: ba 58 24 6b 00 mov $0x6b2458,%edx
40552e: be 80 28 6b 00 mov $0x6b2880,%esi
405533: 48 89 c7 mov %rax,%rdi
405536: 48 89 04 24 mov %rax,(%rsp)
40553a: e8 51 36 05 00 callq 458b90 <rts_apply>
40553f: 48 8d 54 24 08 lea 0x8(%rsp),%rdx
405544: 48 89 c6 mov %rax,%rsi
405547: 48 89 e7 mov %rsp,%rdi
40554a: e8 01 39 05 00 callq 458e50 <rts_evalIO>
40554f: 48 8b 34 24 mov (%rsp),%rsi
405553: bf 64 57 48 00 mov $0x485764,%edi
405558: e8 23 3a 05 00 callq 458f80 <rts_checkSchedStatus>
40555d: 48 8b 3c 24 mov (%rsp),%rdi
405561: e8 0a 3b 05 00 callq 459070 <rts_unlock>
405566: 48 83 c4 18 add $0x18,%rsp
40556a: c3 retq
40556b: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)
405570: d8 ce fmul %st(6),%st
Это выглядит совсем по-другому. Действительно, функции RTS кажутся подозрительными. Посмотрим на них:
-
rts_checkSchedStatus
просто проверяет, является ли состояние в порядке, и выходит иначе. ПутьSuccess
не имеет больших накладных расходов, поэтому эта функция не является виновником. -
rts_unlock
иrts_lock
в основном заявляют и выпускают возможность (виртуальный процессор). Они называютnewBoundTask
иboundTaskExiting
, что занимает некоторое время (см. Профиль выше). -
rts_apply
вызываетallocate
, одну из наиболее часто используемых функций во всей программе (что не удивительно, поскольку Haskell собирает мусор). -
rts_evalIO
окончательно создает фактический поток и ждет его завершения. Таким образом, мы можем оценить, что толькоrts_evalIO
занимает около 27%.
Итак, мы нашли все функции, которые занимают все время. STG и RTS берут на себя всю ответственность за накладные расходы в 150 нс за звонок.
... и есть ли способ смягчить это замедление?
Ну, ваш test
в основном не-op. Вы называете это 100000000 раз, с общей продолжительностью 15 секунд. По сравнению с версией C это накладные расходы ~ 149 нс на звонок.
Решение довольно просто: не используйте функции Haskell в вашем мире C для тривиальных задач. Используйте правильный инструмент для правильной ситуации. В конце концов, вы не используете библиотеку GMP, если вам нужно добавить два числа, которые гарантированно будут меньше 10.
Помимо этого парадигматического решения: нет. Сборка, показанная выше, является тем, что создается GHC, и в настоящий момент невозможно создать вариант без вызовов RTS.