tipos por operadores logicos funciones ejemplos descargar datos casos aritmeticos c performance haskell ffi

operadores - ¿Por qué hay una sobrecarga llamando a las funciones de Haskell desde C?



operadores logicos y aritmeticos en haskell (1)

He notado una sobrecarga significativa al llamar a las funciones de Haskell en C, mucho más grande que la sobrecarga de una llamada a la función C nativa. Para destilar el problema a su esencia, escribí un programa que simplemente inicializa el tiempo de ejecución de Haskell, ejecuta un ciclo que llama a una función vacía 100,000,000 veces y regresa.

Con la función en línea, el programa toma 0.003s. Llamar a una función vacía escrita en C toma 0.18s. Llamar a una función vacía escrita en Haskell toma 15.5s. (Extrañamente, si compilo el archivo Haskell vacío por separado antes de vincularlo, tomará unos segundos más. Subpregunta: ¿por qué es esto?)

Así que parece que hay aproximadamente una ralentización de 100 veces entre llamar a una función C y llamar a una función Haskell. ¿Cuál es la razón de esto, y hay una manera de mitigar esta desaceleración?

Código

EDITAR: He descubierto una versión de esta prueba en la suite de referencia NoFib , callback002 . Hay una buena publicación en el blog de Edward Z. Yang que menciona esta prueba en el contexto del programador de GHC. Todavía estoy tratando de asimilar esta publicación de blog junto con la muy buena respuesta de Zeta. ¡Todavía no estoy convencido de que no haya una manera de hacerlo más rápido!

Para compilar la versión "lenta" de Haskell, ejecute

ghc -no-hs-main -O2 -optc-O3 test.c Test.hs -o test

Para compilar la versión C "rápida", ejecute

ghc -no-hs-main -O2 -optc-O3 test.c test2.c TestDummy.hs -o test

prueba.c:

#include "HsFFI.h" extern void __stginit_Test(void); extern void test(); int main(int argc, char *argv[]) { hs_init(&argc, &argv); hs_add_root(__stginit_Test); int i; for (i = 0; i < 100000000; i++) { test(); } hs_exit(); return 0; }

test2.c:

void test() { }

Prueba.hs:

{-# LANGUAGE ForeignFunctionInterface #-} module Test where foreign export ccall test :: () test :: () test = ()

TestDummy.hs:

module Test where


TL; DR : Motivo: RTS y llamadas STG. Solución: no llamar a las funciones triviales de Haskell desde C.

Cuál es la razón para esto…?

Descargo de responsabilidad: nunca he llamado a Haskell desde C. Estoy familiarizado con C y Haskell, pero rara vez entrelazo ambos, a menos que esté escribiendo un envoltorio. Ahora que he perdido mi credibilidad, comencemos esta aventura de evaluación comparativa, desmontaje y otros horrores ingeniosos.

Benchmarking con gprof

Una forma fácil de verificar qué está consumiendo todos sus recursos es usar gprof. Cambiaremos su línea de compilación de forma que el compilador y el vinculador utilicen -pg (nota: he cambiado el nombre de test.c a main.c y test2.c a test.c):

$ ghc -no-hs-main -O2 -optc-O3 -optc-pg -optl-pg -fforce-recomp / main.c Test.hs -o test $ ./test $ gprof ./test

Esto nos da el siguiente perfil (plano):

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, eso es un montón de funciones que se llaman. ¿Cómo se compara esto con tu versión 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

Eso es mucho más simple. ¿Pero por qué?

¿Qué está pasando detrás?

Tal vez te has preguntado por qué la test incluso apareció en el perfil anterior. Bueno, gprof agrega cierta sobrecarga, como puede verse con 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 <mcount@plt> 405639: 5d pop %rbp 40563a: c3 retq

La llamada de mcount es agregada por gcc. Así que para la siguiente parte desea eliminar las opciones de -pg . Revisemos primero la rutina de test desmontada en 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

El repz retq es en realidad una magia de optimización , pero en este caso, puedes pensar que se trata de un retorno (en su mayoría) no operativo.

¿Cómo se compara esto con la versión de 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

Esto se ve bastante diferente. De hecho, las funciones de RTS parecen sospechosas. Echemos un vistazo a ellos:

  • rts_checkSchedStatus simplemente comprueba si el estado es rts_checkSchedStatus y sale de lo contrario. La ruta del Success no tiene mucha sobrecarga, por lo que esta función no es realmente la culpable.
  • rts_unlock y rts_lock básicamente reclaman y liberan una capability (una CPU virtual). Llaman a newBoundTask y boundTaskExiting , lo que lleva algún tiempo (ver perfil arriba).
  • rts_apply llamadas rts_apply se allocate , una de las funciones más utilizadas en todo el programa (lo que no es realmente sorprendente, ya que se recolecta la basura de Haskell).
  • rts_evalIO finalmente crea el hilo real y espera su finalización. Así que podemos estimar que solo rts_evalIO toma alrededor del 27%.

Así que encontramos todas las funciones que están tomando todo el tiempo. El STG y el RTS se llevan todo el crédito por los gastos generales de 150 ns por llamada.

... y hay una manera de mitigar esta desaceleración?

Bueno, tu test es básicamente un no-op. Lo estás llamando 100000000 veces, con un tiempo de ejecución total de 15s. En comparación con la versión C, es una sobrecarga de ~ 149ns por llamada.

La solución es bastante simple: no utilice las funciones de Haskell en su mundo C para tareas triviales. Utilice la herramienta adecuada para la situación correcta. Después de todo, no usa la biblioteca GMP si necesita agregar dos números que se garantiza que sean menos de 10.

Aparte de esta solución paradigmática: no. El ensamblaje que se muestra arriba es lo que creó GHC, y no es posible crear una variante sin llamadas RTS en este momento.