por qué mi camino no funciona
Primero, las bibliotecas compartidas en UNIX están diseñadas para imitar la forma en que funcionan las bibliotecas de archivos (las bibliotecas de archivos estaban allí primero). En particular, eso significa que si tiene libfoo.so
y libbar.so
, ambos definen el símbolo foo
, la biblioteca que se cargue primero es la que gana:todas las referencias a foo
desde cualquier lugar dentro del programa (incluso desde libbar.so
) se unirá a libfoo.so
s definición de foo
.
Esto imita lo que sucedería si vinculara su programa contra libfoo.a
y libbar.a
, donde ambas bibliotecas de archivo definieron el mismo símbolo foo
. Más información sobre la vinculación de archivos aquí.
Debería quedar claro desde arriba, que si libblas.so.3
y libopenblas.so.0
definen el mismo conjunto de símbolos (que hacen ), y si libblas.so.3
se carga en el proceso primero, luego las rutinas de libopenblas.so.0
nunca ser llamado.
En segundo lugar, ha decidido correctamente que desde R
enlaces directos contra libR.so
, y desde libR.so
enlaces directos contra libblas.so.3
, se garantiza que libopenblas.so.0
perderá la batalla.
Sin embargo, erróneamente decidió que Rscript
es mejor, pero no lo es:Rscript
es un pequeño binario (11K en mi sistema; comparar con 2,4 MB para libR.so
), y aproximadamente todo lo que hace es exec
de R
. Esto es trivial de ver en strace
salida:
strace -e trace=execve /usr/bin/Rscript --default-packages=base --vanilla /dev/null
execve("/usr/bin/Rscript", ["/usr/bin/Rscript", "--default-packages=base", "--vanilla", "/dev/null"], [/* 42 vars */]) = 0
execve("/usr/lib/R/bin/R", ["/usr/lib/R/bin/R", "--slave", "--no-restore", "--vanilla", "--file=/dev/null", "--args"], [/* 43 vars */]) = 0
--- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=89625, si_status=0, si_utime=0, si_stime=0} ---
--- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=89626, si_status=0, si_utime=0, si_stime=0} ---
execve("/usr/lib/R/bin/exec/R", ["/usr/lib/R/bin/exec/R", "--slave", "--no-restore", "--vanilla", "--file=/dev/null", "--args"], [/* 51 vars */]) = 0
--- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=89630, si_status=0, si_utime=0, si_stime=0} ---
+++ exited with 0 +++
Lo que significa que para cuando su secuencia de comandos comience a ejecutarse, libblas.so.3
ha sido cargado, y libopenblas.so.0
que se cargará como una dependencia de mmperf.so
no realmente ser usado para cualquier cosa.
¿Es posible hacer que funcione?
Probablemente. Se me ocurren dos posibles soluciones:
- Pretende que
libopenblas.so.0
en realidad eslibblas.so.3
- Reconstruir todo
R
paquete contralibopenblas.so
.
Para el n.º 1, debe ln -s libopenblas.so.0 libblas.so.3
, entonces asegúrese de que su copia de libblas.so.3
se encuentra antes que el del sistema, configurando LD_LIBRARY_PATH
apropiadamente.
Esto parece funcionar para mí:
mkdir /tmp/libblas
# pretend that libc.so.6 is really libblas.so.3
cp /lib/x86_64-linux-gnu/libc.so.6 /tmp/libblas/libblas.so.3
LD_LIBRARY_PATH=/tmp/libblas /usr/bin/Rscript /dev/null
Error in dyn.load(file, DLLpath = DLLpath, ...) :
unable to load shared object '/usr/lib/R/library/stats/libs/stats.so':
/usr/lib/liblapack.so.3: undefined symbol: cgemv_
During startup - Warning message:
package ‘stats’ in options("defaultPackages") was not found
Tenga en cuenta cómo obtuve un error (mi "fingir" libblas.so.3
no define los símbolos que se esperan de él, ya que en realidad es una copia de libc.so.6
).
También puede confirmar qué versión de libblas.so.3
se está cargando de esta manera:
LD_DEBUG=libs LD_LIBRARY_PATH=/tmp/libblas /usr/bin/Rscript /dev/null |& grep 'libblas\.so\.3'
91533: find library=libblas.so.3 [0]; searching
91533: trying file=/usr/lib/R/lib/libblas.so.3
91533: trying file=/usr/lib/x86_64-linux-gnu/libblas.so.3
91533: trying file=/usr/lib/jvm/java-7-openjdk-amd64/jre/lib/amd64/server/libblas.so.3
91533: trying file=/tmp/libblas/libblas.so.3
91533: calling init: /tmp/libblas/libblas.so.3
Para el #2, dijiste:
No tengo acceso de root en las máquinas que quiero probar, por lo que es imposible vincular a OpenBLAS.
pero eso parece ser un argumento falso:si puedes construir libopenblas
, seguramente también puedes construir tu propia versión de R
.
Actualización:
Mencionaste al principio que libblas.so.3 y libopenblas.so.0 definen el mismo símbolo, ¿qué significa esto? Tienen SONAME diferente, ¿eso es insuficiente para distinguirlos por el sistema?
Los símbolos y el SONAME
no tener nada hacer unos con otros.
Puede ver símbolos en la salida de readelf -Ws libblas.so.3
y readelf -Ws libopenblas.so.0
. Símbolos relacionados con BLAS
, como cgemv_
, aparecerá en ambas bibliotecas.
Tu confusión sobre SONAME
posiblemente viene de Windows. El DLL
s en Windows están diseñados de manera completamente diferente. En particular, cuando FOO.DLL
importa símbolo bar
de BAR.DLL
, ambos el nombre del símbolo (bar
) y el DLL
desde el que se importó ese símbolo (BAR.DLL
) se registran en el FOO.DLL
s tabla de importación.
Eso hace que sea fácil tener R
importar cgemv_
de BLAS.DLL
, mientras que MMPERF.DLL
importa el mismo símbolo de OPENBLAS.DLL
.
Sin embargo, eso dificulta la interposición de bibliotecas y funciona de manera completamente diferente a la forma en que funcionan las bibliotecas de archivos (incluso en Windows).
Las opiniones difieren sobre qué diseño es mejor en general, pero es probable que ningún sistema cambie su modelo.
Hay formas en que UNIX puede emular el enlace de símbolos al estilo de Windows:consulte RTLD_DEEPBIND
en la página man de dlopen. Cuidado:estos están llenos de peligros, es probable que confundan a los expertos de UNIX, no se usan mucho y es probable que tengan errores de implementación.
Actualización 2:
¿Quieres decir que compilo R y lo instalo en mi directorio de inicio?
Sí.
Luego, cuando quiero invocarlo, debería dar explícitamente la ruta a mi versión del programa ejecutable, de lo contrario, ¿se podría invocar el del sistema? ¿O puedo poner esta ruta en la primera posición de la variable de entorno $PATH para engañar al sistema?
De cualquier manera funciona.
********************
Solución 1:
********************
Gracias a Employeeed Russian, mi problema finalmente se resolvió. La investigación requiere habilidades importantes en depuración y parches del sistema Linux , y creo que esto es un gran activo que aprendí. Aquí publicaría una solución, además de corregir varios puntos en mi publicación original.
1 Acerca de la invocación de R
En mi publicación original, mencioné que hay dos formas de iniciar R, ya sea a través de R
o Rscript
. Sin embargo, he exagerado erróneamente su diferencia. Ahora investiguemos su proceso de inicio, a través de una importante función de depuración de Linux strace
(ver man strace
). En realidad, suceden muchas cosas interesantes después de que escribimos un comando en el shell, y podemos usar
strace -e trace=process [command]
rastrear todas las llamadas al sistema relacionadas con la gestión de procesos. Como resultado, podemos observar los pasos de bifurcación, espera y ejecución de un proceso. Aunque no se indica en la página del manual, @Employed Russian muestra que es posible especificar solo una subclase de process
, por ejemplo, execve
para los pasos de ejecución.
Para R
tenemos
~/Desktop/dgemm$ time strace -e trace=execve R --vanilla < /dev/null > /dev/null
execve("/usr/bin/R", ["R", "--vanilla"], [/* 70 vars */]) = 0
--- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=5777, si_status=0, si_utime=0, si_stime=0} ---
execve("/usr/lib/R/bin/exec/R", ["/usr/lib/R/bin/exec/R", "--vanilla"], [/* 79 vars */]) = 0
--- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=5778, si_status=0, si_utime=0, si_stime=0} ---
+++ exited with 0 +++
real 0m0.345s
user 0m0.256s
sys 0m0.068s
mientras que para Rscript
tenemos
~/Desktop/dgemm$ time strace -e trace=execve Rscript --default-packages=base --vanilla /dev/null
execve("/usr/bin/Rscript", ["Rscript", "--default-packages=base", "--vanilla", "/dev/null"], [/* 70 vars */]) = 0
execve("/usr/lib/R/bin/R", ["/usr/lib/R/bin/R", "--slave", "--no-restore", "--vanilla", "--file=/dev/null"], [/* 71 vars */]) = 0
--- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=5822, si_status=0, si_utime=0, si_stime=0} ---
--- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=5823, si_status=0, si_utime=0, si_stime=0} ---
execve("/usr/lib/R/bin/exec/R", ["/usr/lib/R/bin/exec/R", "--slave", "--no-restore", "--vanilla", "--file=/dev/null"], [/* 80 vars */]) = 0
--- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=5827, si_status=0, si_utime=0, si_stime=0} ---
+++ exited with 0 +++
real 0m0.063s
user 0m0.020s
sys 0m0.028s
También hemos usado time
para medir el tiempo de arranque. Tenga en cuenta que
Rscript
es aproximadamente 5,5 veces más rápido queR
. Una razón es queR
cargará 6 paquetes predeterminados al inicio, mientras queRscript
solo carga unbase
paquete por control:--default-packages=base
. Pero sigue siendo mucho más rápido incluso sin esta configuración.- Al final, ambos procesos de inicio se dirigen a
$(R RHOME)/bin/exec/R
, y en mi publicación original, ya he explotadoreadelf -d
para mostrar que este ejecutable cargarálibR.so
, que están vinculados conlibblas.so.3
. De acuerdo con la explicación de @Employed Russian, la biblioteca BLAS cargada primero ganará, por lo que no hay forma de que mi método original funcione. - Para ejecutar con éxito
strace
, hemos utilizado el increíble archivo/dev/null
como archivo de entrada y archivo de salida cuando sea necesario. Por ejemplo,Rscript
exige un archivo de entrada, mientras queR
exige ambos. Alimentamos el dispositivo nulo a ambos para que el comando se ejecute sin problemas y la salida sea limpia. El dispositivo nulo es un archivo físicamente existente, pero funciona de manera sorprendente. Al leerlo, no contiene nada; mientras escribe en él, descarta todo.
2. Hacer trampa R
Ahora desde libblas.so
se cargará de todos modos, lo único que podemos hacer es proporcionar nuestra propia versión de esta biblioteca. Como dije en la publicación original, si tenemos acceso de root, esto es realmente fácil, usando update-alternatives --config libblas.so.3
, por lo que el sistema Linux nos ayudará a completar este cambio. Pero @Employed Russian ofrece una forma increíble de engañar al sistema sin acceso a la raíz:¡Veamos cómo R encuentra la biblioteca BLAS en el inicio y asegurémonos de alimentar nuestra versión antes de que se encuentre el sistema predeterminado! Para monitorear cómo se encuentran y cargan las bibliotecas compartidas, use la variable de entorno LD_DEBUG
.
Hay una serie de variables de entorno de Linux con el prefijo LD_
, como se documenta en man ld.so
. Estas variables se pueden asignar antes de un ejecutable, de modo que podamos cambiar la función de ejecución de un programa. Algunas variables útiles incluyen:
LD_LIBRARY_PATH
para configurar la ruta de búsqueda de la biblioteca en tiempo de ejecución;LD_DEBUG
para rastrear, buscar y cargar bibliotecas compartidas;LD_TRACE_LOADED_OBJECTS
para mostrar toda la biblioteca cargada por un programa (se comporta de manera similar aldd
);LD_PRELOAD
para forzar la inyección de una biblioteca en un programa desde el principio, antes de que se busquen todas las demás bibliotecas;LD_PROFILE
yLD_PROFILE_OUTPUT
para perfilar uno biblioteca compartida especificada. Usuario R que ha leído la sección 3.4.1.1 sprof of Writing R extensions debe recordar que esto se usa para generar perfiles de código compilado desde dentro de R.
El uso de LD_DEBUG
puede ser visto por:
~/Desktop/dgemm$ LD_DEBUG=help cat
Valid options for the LD_DEBUG environment variable are:
libs display library search paths
reloc display relocation processing
files display progress for input file
symbols display symbol table processing
bindings display information about symbol binding
versions display version dependencies
scopes display scope information
all all previous options combined
statistics display relocation statistics
unused determined unused DSOs
help display this help message and exit
To direct the debugging output into a file instead of standard output a filename can be specified using the LD_DEBUG_OUTPUT environment variable.
Aquí estamos particularmente interesados en usar LD_DEBUG=libs
. Por ejemplo,
~/Desktop/dgemm$ LD_DEBUG=libs Rscript --default-packages=base --vanilla /dev/null |& grep blas
5974: find library=libblas.so.3 [0]; searching
5974: trying file=/usr/lib/R/lib/libblas.so.3
5974: trying file=/usr/lib/i386-linux-gnu/i686/sse2/libblas.so.3
5974: trying file=/usr/lib/i386-linux-gnu/i686/cmov/libblas.so.3
5974: trying file=/usr/lib/i386-linux-gnu/i686/libblas.so.3
5974: trying file=/usr/lib/i386-linux-gnu/sse2/libblas.so.3
5974: trying file=/usr/lib/i386-linux-gnu/libblas.so.3
5974: trying file=/usr/lib/jvm/java-7-openjdk-i386/jre/lib/i386/client/libblas.so.3
5974: trying file=/usr/lib/libblas.so.3
5974: calling init: /usr/lib/libblas.so.3
5974: calling fini: /usr/lib/libblas.so.3 [0]
muestra varios intentos que el programa R intentó localizar y cargar libblas.so.3
. Entonces, si pudiéramos proporcionar nuestra propia versión de libblas.so.3
y asegúrese de que R lo encuentre primero, entonces el problema está resuelto.
Primero hagamos un enlace simbólico libblas.so.3
en nuestra ruta de trabajo a la biblioteca OpenBLAS libopenblas.so
, luego expanda el LD_LIBRARY_PATH
predeterminado con nuestra ruta de trabajo (y exportar es):
~/Desktop/dgemm$ ln -sf libopenblas.so libblas.so.3
~/Desktop/dgemm$ export LD_LIBRARY_PATH = $(pwd):$LD_LIBRARY_PATH ## put our working path at top
Ahora revisemos nuevamente el proceso de carga de la biblioteca:
~/Desktop/dgemm$ LD_DEBUG=libs Rscript --default-packages=base --vanilla /dev/null |& grep blas
6063: find library=libblas.so.3 [0]; searching
6063: trying file=/usr/lib/R/lib/libblas.so.3
6063: trying file=/usr/lib/i386-linux-gnu/i686/sse2/libblas.so.3
6063: trying file=/usr/lib/i386-linux-gnu/i686/cmov/libblas.so.3
6063: trying file=/usr/lib/i386-linux-gnu/i686/libblas.so.3
6063: trying file=/usr/lib/i386-linux-gnu/sse2/libblas.so.3
6063: trying file=/usr/lib/i386-linux-gnu/libblas.so.3
6063: trying file=/usr/lib/jvm/java-7-openjdk-i386/jre/lib/i386/client/libblas.so.3
6063: trying file=/home/zheyuan/Desktop/dgemm/libblas.so.3
6063: calling init: /home/zheyuan/Desktop/dgemm/libblas.so.3
6063: calling fini: /home/zheyuan/Desktop/dgemm/libblas.so.3 [0]
¡Excelente! Hemos engañado con éxito a R.
3. Experimente con OpenBLAS
~/Desktop/dgemm$ Rscript --default-packages=base --vanilla mmperf.R
GFLOPs = 8.77
¡Ahora, todo funciona como se esperaba!
4. Desarmar LD_LIBRARY_PATH
(estar a salvo)
Es una buena práctica desarmar LD_LIBRARY_PATH
después de su uso.
~/Desktop/dgemm$ unset LD_LIBRARY_PATH
********************
Solución 2:
********************
Aquí ofrecemos otra solución, explotando la variable de entorno LD_PRELOAD
mencionado en nuestra solución 1 . El uso de LD_PRELOAD
es más "brutal", ya que obliga a cargar una biblioteca dada en el programa antes que cualquier otro programa, incluso antes que la biblioteca C libc.so
! Esto se usa a menudo para parches urgentes en el desarrollo de Linux.
Como se muestra en la parte 2 de la publicación original , la biblioteca BLAS compartida libopenblas.so
tiene SONAME libopenblas.so.0
. Un SONAME es un nombre interno que el cargador de bibliotecas dinámicas buscaría en tiempo de ejecución, por lo que debemos crear un enlace simbólico a libopenblas.so
con este SONAME :
~/Desktop/dgemm$ ln -sf libopenblas.so libopenblas.so.0
luego lo exportamos:
~/Desktop/dgemm$ export LD_PRELOAD=$(pwd)/libopenblas.so.0
Tenga en cuenta que una ruta completa a libopenblas.so.0
debe enviarse a LD_PRELOAD
para una carga exitosa, incluso si libopenblas.so.0
está bajo $(pwd)
.
Ahora lanzamos Rscript
y verifique lo que sucede por LD_DEBUG
:
~/Desktop/dgemm$ LD_DEBUG=libs Rscript --default-packages=base --vanilla /dev/null |& grep blas
4860: calling init: /home/zheyuan/Desktop/dgemm/libopenblas.so
4860: calling init: /home/zheyuan/Desktop/dgemm/libopenblas.so
4865: calling init: /home/zheyuan/Desktop/dgemm/libopenblas.so
4868: calling fini: /home/zheyuan/Desktop/dgemm/libopenblas.so [0]
4870: calling init: /home/zheyuan/Desktop/dgemm/libopenblas.so
4869: calling init: /home/zheyuan/Desktop/dgemm/libopenblas.so
4867: calling fini: /home/zheyuan/Desktop/dgemm/libopenblas.so [0]
4860: find library=libblas.so.3 [0]; searching
4860: trying file=/usr/lib/R/lib/libblas.so.3
4860: trying file=/usr/lib/i386-linux-gnu/i686/sse2/libblas.so.3
4860: trying file=/usr/lib/i386-linux-gnu/i686/cmov/libblas.so.3
4860: trying file=/usr/lib/i386-linux-gnu/i686/libblas.so.3
4860: trying file=/usr/lib/i386-linux-gnu/sse2/libblas.so.3
4860: trying file=/usr/lib/i386-linux-gnu/libblas.so.3
4860: trying file=/usr/lib/jvm/java-7-openjdk-i386/jre/lib/i386/client/libblas.so.3
4860: trying file=/usr/lib/libblas.so.3
4860: calling init: /usr/lib/libblas.so.3
4860: calling init: /home/zheyuan/Desktop/dgemm/libopenblas.so
4874: calling init: /home/zheyuan/Desktop/dgemm/libopenblas.so
4876: calling init: /home/zheyuan/Desktop/dgemm/libopenblas.so
4860: calling fini: /home/zheyuan/Desktop/dgemm/libopenblas.so [0]
4860: calling fini: /usr/lib/libblas.so.3 [0]
Comparando con lo que vimos en la solución 1 engañando a R con nuestra propia versión de libblas.so.3
, podemos ver que
libopenblas.so.0
se carga primero, por lo tanto, se encuentra primero porRscript
;- después de
libopenblas.so.0
se encuentra,Rscript
continúa buscando y cargandolibblas.so.3
. Sin embargo, esto no afectará al "primero en llegar, primero en servir" regla, explicada en la respuesta original.
Bien, todo funciona, así que probamos nuestro mmperf.c
programa:
~/Desktop/dgemm$ Rscript --default-packages=base --vanilla mmperf.R
GFLOPs = 9.62
El resultado 9,62 es mayor que el 8,77 que vimos en la solución anterior simplemente por casualidad. Como prueba para usar OpenBLAS, no ejecutamos el experimento muchas veces para obtener un resultado más preciso.
Luego, como de costumbre, desactivamos la variable de entorno al final:
~/Desktop/dgemm$ unset LD_PRELOAD