proč můj způsob nefunguje
Za prvé, sdílené knihovny na UNIXu jsou navrženy tak, aby napodobovaly způsob, jakým fungují archivní knihovny (archivní knihovny tu byly první). Konkrétně to znamená, že pokud máte libfoo.so
a libbar.so
, oba definují symbol foo
, pak vyhraje knihovna, která se načte jako první:všechny odkazy na foo
odkudkoli v programu (včetně z libbar.so
) se naváže na libfoo.so
s definice foo
.
To napodobuje, co by se stalo, kdybyste svůj program propojili s libfoo.a
a libbar.a
, kde obě archivní knihovny definovaly stejný symbol foo
. Více informací o propojení archivu zde.
Shora by mělo být jasné, že pokud libblas.so.3
a libopenblas.so.0
definovat stejnou sadu symbolů (což dělají ), a pokud libblas.so.3
je nejprve načten do procesu a poté rutiny z libopenblas.so.0
nikdy být volán.
Za druhé, správně jste se rozhodli, že od R
přímé odkazy proti libR.so
a od libR.so
přímé odkazy proti libblas.so.3
, je zaručeno, že libopenblas.so.0
prohraje bitvu.
Vy však omylně rozhodl, že Rscript
je lepší, ale není:Rscript
je maličkost binární (11 kB v mém systému; srovnání s 2,4 MB pro libR.so
) a přibližně vše, co dělá, je exec
z R
. To je triviální vidět v strace
výstup:
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 +++
Což znamená, že v době, kdy se váš skript spustí, bude libblas.so.3
byl načten a libopenblas.so.0
která bude načtena jako závislost mmperf.so
ve skutečnosti nebude být použit na cokoli.
je vůbec možné, aby to fungovalo
Pravděpodobně. Napadají mě dvě možná řešení:
- Předstírejte, že
libopenblas.so.0
je ve skutečnostilibblas.so.3
- Znovu sestavte celý
R
balíček protilibopenblas.so
.
Pro číslo 1 potřebujete ln -s libopenblas.so.0 libblas.so.3
, pak ujistěte se, že vaše kopie libblas.so.3
se najde před systémovým nastavením LD_LIBRARY_PATH
přiměřeně.
Zdá se, že toto pro mě funguje:
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
Všimněte si, jak jsem dostal chybu (můj „předstírat“ libblas.so.3
nedefinuje symboly, které se od něj očekávají, protože se ve skutečnosti jedná o kopii libc.so.6
).
Můžete také potvrdit, která verze libblas.so.3
se načítá tímto způsobem:
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
Pro číslo 2 jste řekli:
Nemám root přístup na počítačích, které chci testovat, takže skutečné propojení s OpenBLAS je nemožné.
ale to se zdá být falešný argument:pokud dokážete sestavit libopenblas
, jistě si také můžete vytvořit svou vlastní verzi R
.
Aktualizace:
Na začátku jste zmínil, že libblas.so.3 a libopenblas.so.0 definují stejný symbol, co to znamená? Mají odlišné SONAME, nestačí to k jejich odlišení systémem?
Symboly a SONAME
nemají nic k sobě navzájem.
Ve výstupu z readelf -Ws libblas.so.3
můžete vidět symboly a readelf -Ws libopenblas.so.0
. Symboly související s BLAS
, například cgemv_
, se objeví v obou knihovnách.
Vaše nejasnosti ohledně SONAME
možná pochází z Windows. DLL
s ve Windows jsou navrženy úplně jinak. Zejména když FOO.DLL
importuje symbol bar
z BAR.DLL
, obě název symbolu (bar
) a DLL
ze kterého byl tento symbol importován (BAR.DLL
) jsou zaznamenány v FOO.DLL
s tabulkou importu.
Díky tomu je snadné mít R
import cgemv_
z BLAS.DLL
, zatímco MMPERF.DLL
importuje stejný symbol z OPENBLAS.DLL
.
To však ztěžuje vkládání knihoven a funguje zcela jinak, než jak fungují archivní knihovny (dokonce i ve Windows).
Názory na to, který design je celkově lepší, se různí, ale ani jeden systém pravděpodobně svůj model nikdy nezmění.
Existují způsoby, jak UNIX emulovat vazbu symbolů ve stylu Windows:viz RTLD_DEEPBIND
v manuálové stránce dlopen. Pozor:tyto jsou plné nebezpečí, pravděpodobně zmást odborníky na UNIX, nejsou široce používány a pravděpodobně obsahují chyby v implementaci.
Aktualizace 2:
myslíte, že zkompiluji R a nainstaluji jej do svého domovského adresáře?
Ano.
Když jej pak chci vyvolat, měl bych výslovně uvést cestu ke své verzi spustitelného programu, jinak by se místo toho mohla vyvolat ta v systému? Nebo mohu dát tuto cestu na první pozici proměnné prostředí $PATH, abych podváděl systém?
Oba způsoby fungují.
***********************
Řešení 1:
***********************
Díky Employed Russian je můj problém konečně vyřešen. Vyšetřování vyžaduje důležité dovednosti v oblasti ladění a oprav systému Linux a věřím, že je to velký přínos, který jsem se naučil. Zde bych zveřejnil řešení a také opravil několik bodů v mém původním příspěvku.
1 O vyvolání R
Ve svém původním příspěvku jsem zmínil, že existují dva způsoby, jak spustit R, buď přes R
nebo Rscript
. Jejich rozdíl jsem však neprávem zveličil. Pojďme nyní prozkoumat jejich proces spouštění prostřednictvím důležitého linuxového ladícího zařízení strace
(viz man strace
). Ve skutečnosti se po zadání příkazu do shellu děje spousta zajímavých věcí, které můžeme použít
strace -e trace=process [command]
ke sledování všech systémových volání zahrnujících řízení procesů. V důsledku toho můžeme sledovat kroky rozvětvení, čekání a provádění procesu. Ačkoli to není uvedeno v manuálové stránce, @Employed Russian ukazuje, že je možné zadat pouze podtřídu process
, například execve
pro kroky provedení.
Pro R
máme
~/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
zatímco pro Rscript
máme
~/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
Také jsme použili time
k měření doby spouštění. Všimněte si, že
Rscript
je asi 5,5krát rychlejší nežR
. Jedním z důvodů jeR
načte 6 výchozích balíčků při spuštění, zatímcoRscript
načte pouze jedenbase
balíček ovládáním:--default-packages=base
. Ale i bez tohoto nastavení je stále mnohem rychlejší.- Nakonec jsou oba spouštěcí procesy přesměrovány na
$(R RHOME)/bin/exec/R
a ve svém původním příspěvku jsem již využilreadelf -d
ukázat, že tento spustitelný soubor načtelibR.so
, které jsou propojeny slibblas.so.3
. Podle vysvětlení @Employed Russian vyhraje knihovna BLAS načtená jako první, takže moje původní metoda nebude fungovat. - Pro úspěšné spuštění
strace
, použili jsme úžasné soubor/dev/null
jako vstupní soubor a v případě potřeby výstupní soubor. NapříkladRscript
vyžaduje vstupní soubor, zatímcoR
požaduje obojí. Nulové zařízení přivádíme do obou, aby příkaz běžel hladce a výstup byl čistý. Nulové zařízení je fyzicky existující soubor, ale funguje úžasně. Při čtení z něj neobsahuje nic; při zápisu do něj vše zahodí.
2. Cheat R
Nyní od libblas.so
se stejně načte, jediné, co můžeme udělat, je poskytnout naši vlastní verzi této knihovny. Jak jsem řekl v původním příspěvku, pokud máme přístup root, je to opravdu snadné pomocí update-alternatives --config libblas.so.3
, takže nám systém Linux pomůže tento přechod dokončit. @Employed Russian ale nabízí úžasný způsob, jak podvádět systém bez přístupu root:podívejme se, jak R najde knihovnu BLAS při spuštění, a ujistěte se, že nahrajeme naši verzi dříve, než bude nalezeno výchozí nastavení systému! Chcete-li sledovat, jak jsou sdílené knihovny nalezeny a načítány, použijte proměnnou prostředí LD_DEBUG
.
Existuje řada proměnných prostředí Linux s předponou LD_
, jak je zdokumentováno v man ld.so
. Tyto proměnné lze přiřadit před spustitelný soubor, takže můžeme změnit funkci běhu programu. Některé užitečné proměnné zahrnují:
LD_LIBRARY_PATH
pro nastavení vyhledávací cesty knihovny běhu;LD_DEBUG
pro sledování vyhledávání a načítání sdílených knihoven;LD_TRACE_LOADED_OBJECTS
pro zobrazení všech načtených knihoven programem (chová se podobně jakoldd
);LD_PRELOAD
za vynucení vložení knihovny do programu na úplném začátku, ještě předtím, než budou vyhledány všechny ostatní knihovny;LD_PROFILE
aLD_PROFILE_OUTPUT
pro profilování jeden zadaná sdílená knihovna. Uživatel R, který si přečetl sekci 3.4.1.1 sprof rozšíření Writing R by mělo připomenout, že se používá pro profilování kompilovaného kódu z R.
Použití LD_DEBUG
může být viděn:
~/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.
Zde nás zvláště zajímá použití LD_DEBUG=libs
. Například,
~/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]
ukazuje různé pokusy, které se program R pokusil najít a načíst libblas.so.3
. Pokud bychom tedy mohli poskytnout naši vlastní verzi libblas.so.3
a ujistěte se, že jej R najde jako první, pak je problém vyřešen.
Udělejme nejprve symbolický odkaz libblas.so.3
v naší pracovní cestě ke knihovně OpenBLAS libopenblas.so
a poté rozbalte výchozí LD_LIBRARY_PATH
s naší pracovní cestou (a exportem to):
~/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
Nyní znovu zkontrolujeme proces načítání knihovny:
~/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]
Skvělý! Úspěšně jsme podvedli R.
3. Experimentujte s OpenBLAS
~/Desktop/dgemm$ Rscript --default-packages=base --vanilla mmperf.R
GFLOPs = 8.77
Nyní vše funguje podle očekávání!
4. Zrušte nastavení LD_LIBRARY_PATH
(pro jistotu)
Je dobrým zvykem zrušit nastavení LD_LIBRARY_PATH
po použití.
~/Desktop/dgemm$ unset LD_LIBRARY_PATH
***********************
Řešení 2:
***********************
Zde nabízíme další řešení, a to využitím proměnné prostředí LD_PRELOAD
uvedené v našem řešení 1 . Použití LD_PRELOAD
je "brutálnější", protože nutí načíst danou knihovnu do programu před jakýmkoli jiným programem, dokonce i před knihovnou C libc.so
! To se často používá pro urgentní opravy při vývoji Linuxu.
Jak je uvedeno v části 2 původního příspěvku , sdílená knihovna BLAS libopenblas.so
má SONAME libopenblas.so.0
. SONAME je interní název, který by zavaděč dynamické knihovny hledal za běhu, takže musíme vytvořit symbolický odkaz na libopenblas.so
s tímto SONAME :
~/Desktop/dgemm$ ln -sf libopenblas.so libopenblas.so.0
pak jej exportujeme:
~/Desktop/dgemm$ export LD_PRELOAD=$(pwd)/libopenblas.so.0
Všimněte si, že úplná cesta na libopenblas.so.0
musí být napájeno na LD_PRELOAD
pro úspěšné načtení, i když libopenblas.so.0
je pod $(pwd)
.
Nyní spustíme Rscript
a zkontrolujte, co se stane do 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]
Porovnání s tím, co jsme viděli v řešení 1 podváděním R s naší vlastní verzí libblas.so.3
, to vidíme
libopenblas.so.0
je načten jako první, tedy nalezen jako první pomocíRscript
;- po
libopenblas.so.0
je nalezen,Rscript
pokračuje ve vyhledávání a načítánílibblas.so.3
. Toto však nebude mít žádný vliv na „kdo dřív přijde, je dřív na řadě“ pravidlo, vysvětlené v původní odpovědi.
Dobře, všechno funguje, takže testujeme naše mmperf.c
program:
~/Desktop/dgemm$ Rscript --default-packages=base --vanilla mmperf.R
GFLOPs = 9.62
Výsledek 9,62 je větší než 8,77, který jsme viděli v dřívějším řešení pouze náhodou. Jako test pro použití OpenBLAS nespouštíme experiment mnohokrát pro přesnější výsledek.
Potom jako obvykle na konci zrušíme nastavení proměnné prostředí:
~/Desktop/dgemm$ unset LD_PRELOAD