aber auch subtile Probleme, etwa Compile-Prozesse, die je nach
2
CPU-Auslastung zu anderen Ergebnissen kommen.
Nach meinem Verstaendnis ist das so, als wenn man in einer Matheklausur
auf Grund von Stress/Zeitdruck (hohe CPU-Auslastung) bei der Aufgabe
"Wie viel ist 1+1?" 3 statt 2 schreibt. In dem Falle ist man zu einem
anderen Ergebnis gekommen, was aber ganz klar falsch ist, und (in diesem
schlechten Beispiel?) einen Fehler im Compiler offenbaren wuerde.
Gleiches wuerde gelten, wenn mir einer "Good morning" mit "Gute Nacht"
uebersetzten wuerde.
Ist das tatsaechlich so, oder ist der Artikel einfach schlecht/falsch
geschrieben? Wuerde mich irgendwie schwer wundern, wenn die Auslastung
der CPU auswirkungen auf das Kompilat haben. Denn das haette ja
irgendwie zur Folge, dass ich als Nutzer faktisch niemals zum selben
(bitidentischen) Build-Ergebnis kommen kann, da mein PC garantiert
voellig anders ausgelastet ist, als irgendein Build-Server mit seinen
96-Cores und +300GB-RAM.
Kann hier jemand Licht ins Dunkel bringen?
Gruesse
Von nichtdeterministischer Codeerzeugung in Compilern habe ich
jedenfalls noch nichts gehört. Aber wenn beispielsweise der
Build-Prozess Entscheidungen aufgrund von Laufzeitest trifft, dann kann
das davon abhängen, was sonst noch auf dem Rechner los ist. Also wenn
beispielsweise ein paar Code-Varianten durchprobiert werden, um den
schnellsten Blocktransfer im Speicher zu ermitteln.
Kaj G. schrieb:> faktisch niemals zum selben> (bitidentischen) Build-Ergebnis kommen kann,
Das hatte ich wohl auch schon mal gelesen -- und nicht von Dummköpfen.
Liegt das nicht u.a. daran, dass der Compiler seine Arbeit auf meherer
Prozesse aufteilt, und welcher Prozess dann was generiert kann
unterschiedlich sein. Natürlich muss das Programm sich stets identisch
verhalten, aber der Code kann sich ja durchaus unterscheiden, womöglich
liegen Segmente einfach an anderer Stelle. Ich hatte da nicht lange
drüber nachgedacht, erschien mir aber plausibel.
Moin,
Ich glaub', da muss man ganz arg obacht geben, was jeweils genau gemeint
ist.
Build != Compile
und
Compile(im Sinne eines gcc-Aufrufs) != Compile im Sinne eines
(cc1/cc1plus-Aufrufs)
Der "Build" ist ueblicherweise einiges mehr als "nur" Aufrufe von gcc
durch make; wenn da z.b. ein tar oder cpio aufgerufen wird, dann sind da
spaeter alle Zeitstempel und user/group id der archivierten files
drinnen. Wird das dann noch irgendwie komprimiert, sieht das binary in
dem der Kram dann landet danach an der Stelle voellig anders aus. Sowas
passiert z.b. beim Linuxkernel bauen.
SAlewski schrieb:> Ganz entscheidend ist wohl auch der freie Arbeitsspeicher:>>> http://stackoverflow.com/questions/1221185/identical-build-on-different-systems
Hmmm...Ok, ich kann's nicht falsifizieren, aber die Erklaerung mit dem
nicht genuegend Speicher kann ich nicht so auf Anhieb glauben. Hat da
zufaellig irgendwer einen Hinweis auf eine Stelle in den gcc-sourcen
parat?
Ich weiss nicht, wie's genau bei Windows ist, aber bei Linux ohne
irgendwelche Spezial- und Sicherheitsfaxen hab' ich eher so den
Eindruck, dass ich als userland process soviel Speicher allokieren kann,
bis der Swap ausgeht. Wenn das passiert, wird vom Kernel ein Prozess
ausgeguckt, der "dran schuld ist", dass es keinen Speicher mehr gibt,
dieser Prozess wird beendet und der von ihm allokierte Speicher wird
freigegeben. Wenn sowas beim compilieren passieren wuerde - das wuerde
arg auffallen...
Und lange vorher schon, wenn "nur" der compiler oder auch der linker
anfaengt zu swappen und genug swap da ist, dann wird der compiler so
dermassen zaeh', dass das auch keiner mehr haben will. (Ich hab' vor
Jahren da mal einschlaegige Erfahrungen gemacht:
http://www.linuxforen.de/forums/showthread.php?204055 und auch sonst
schon auf schwachbruestigen Systemen Zeugs gebaut).
Was aber wohl leicht passieren kann, ist, dass make der Ansicht ist, man
koennte mehrere compileraufrufe parallelisiert ausfuehren. Dann laufen
z.b. 4x der gcc los, z.b. und beschaeftigen sich mit z.b. 4 Files namens
bla1.c, bla2.c bla3.c bla4.c . Die koennten dann verschieden schnell
(z.b. abhaengig von der CPU Last durch andere Prozesse) in entsprechende
.o files verwandelt worden sein. Beim Linkeraufruf koennte z.b. sowas
stehen:
gcc -o bla bla*o
Und damit wuerd' ich sagen, ist die Reihenfolge, mit der das Zeugs
gelinkt wird, nicht mehr klar definiert. So ein Verhalten koennte zu den
in dem Stackoverflowlink behandelten Effekten fuehren...
Aber vielleicht isses auch einfach nur grad viel zu spaet oder zu frueh
und ich schreib' grossen Quatsch...
Gruss
WK
Kaj G. schrieb:> wird geschrieben, das sich Compile-Ergibnise aufgrund der CPU-Auslastung> unterscheiden koennen.>
1
> aber auch subtile Probleme, etwa Compile-Prozesse, die je nach
2
> CPU-Auslastung zu anderen Ergebnissen kommen.
3
>
Ich könnte mir vorstellen, dass es zum Beispiel dann eine Abhängigkeit
gibt, wenn der Compiler in der Optimierungsphase unetrschiedliche
Codevarianten bruth force mässig durchprobiert um zu sehen, welche davon
die beste Pipeline Auslastung ergibt. Das ist ein nicht-triviales
Problem und ich bin mir nicht sicher, ob es da saubere Algorithmen gibt.
> Nach meinem Verstaendnis ist das so, als wenn man in einer Matheklausur> auf Grund von Stress/Zeitdruck (hohe CPU-Auslastung) bei der Aufgabe> "Wie viel ist 1+1?" 3 statt 2 schreibt.
Nicht ganz.
Das ist eher so, wie beim Traveling Salesman Problem, bei dem man mit
längerer Laufzeit unter Umständen ein Ergebnis kriegt, welches ein paar
Zehntelprozentpunkte besser ist als das, was man schon hat. Beide
Ergebnisse sind aber gültige und korrekte Rundreisen. Nur das die eine
eben ein bischen schneller ist.
Fehlerhaft arbeitet der Compiler deswegen nicht. Das wäre unbrauchbar.
Interessant.
Bis vorhin grade hätte ich noch gewettet, daß wenn man den Compiler
mehrfach auf den selben Quellcode losläßt, auch immer dasselbe
rauskommt.
Ist aber offensichtlich nicht so. Das Compilerergebnis ist
nondeterministisch...
Mit "Last beim Compilieren" hat das aber wohl nichts zu tun, zumindest
wenn man der gcc-Dokumentation Glauben schenkt. Es scheint, es gibt
Funktionen im Optimizer, die einfach "zufällig rumprobieren":
-fno-guess-branch-probability/
Do not guess branch probabilities using a randomized model.
/Sometimes gcc will opt to use a randomized model to guess branch
probabilities, when none are available from either profiling feedback
(-fprofile-arcs) or __builtin_expect. This means that different runs of
the compiler on the same program may produce different object code.
In a hard real-time system, people don't want different runs of the
compiler to produce code that has different behavior; minimizing
non-determinism is of paramount import. This switch allows users to
reduce non-determinism, possibly at the expense of inferior
optimization.
The default is -fguess-branch-probability at levels -O, -O2, -O3,
-Os.
Wenn man immer dasselbe Ergebnis haben will, sollte man wohl
-fno-guess-branch-probability spezifizieren.
Ok, gut zu wissen das ich nicht der Einzige bin, dem das nicht ganz klar
ist :)
Markus F. schrieb:> Bis vorhin grade hätte ich noch gewettet, daß wenn man den Compiler> mehrfach auf den selben Quellcode losläßt, auch immer dasselbe> rauskommt.
Das ist zumindestens das, wo von auch ich bisher ausgegangen bin bzw.
erwartet habe.
Karl Heinz schrieb:> Nicht ganz.> Das ist eher so, wie beim Traveling Salesman Problem, bei dem man mit> längerer Laufzeit unter Umständen ein Ergebnis kriegt, welches ein paar> Zehntelprozentpunkte besser ist als das, was man schon hat. Beide> Ergebnisse sind aber gültige und korrekte Rundreisen. Nur das die eine> eben ein bischen schneller ist.>> Fehlerhaft arbeitet der Compiler deswegen nicht. Das wäre unbrauchbar.
Ok, dann war mein Beispiel schlecht, danke fuer das bessere Beispiel :)
derguteweka schrieb:> Hmmm...Ok, ich kann's nicht falsifizieren, aber die Erklaerung mit dem> nicht genuegend Speicher kann ich nicht so auf Anhieb glauben.
Man ist ja von Microsoft so einige Seltsamkeiten gewohnt, aber das da
kann ich mir dann irgendwie auch nicht vorstellen:
If on one machine a memory allocation fails (because the machine has
less memory) then the compiler could omit that specific optimisation,
resulting in different code being emitted.
Wenn der Compiler nicht genug Speicher hat, wird er ja hoffentlich nicht
einfach eigenmächtig was anderes machen, das weniger Speicher braucht.
> Ich weiss nicht, wie's genau bei Windows ist, aber bei Linux ohne> irgendwelche Spezial- und Sicherheitsfaxen hab' ich eher so den> Eindruck, dass ich als userland process soviel Speicher allokieren kann,> bis der Swap ausgeht.
Du kannst sogar mehr allokieren. Probleme gibt's erst dann, wenn der
Speicher auch benutzt wird. Das nennt sich "memory overcommit". Das wird
gemacht, weil es viele Programme gibt, die Speicher quasi auf Vorrat
allokieren und große Teile davon nacher nie brauchen.
> Wenn das passiert, wird vom Kernel ein Prozess ausgeguckt, der "dran> schuld ist", dass es keinen Speicher mehr gibt, dieser Prozess wird> beendet und der von ihm allokierte Speicher wird freigegeben.
Genau. Das nennt sich "OOM (out-of-memory) kill".
> Wenn sowas beim compilieren passieren wuerde - das wuerde arg> auffallen...
Der Compiler nutzt zwar mehrere Unterprozesse, aber wenn einer davon
gekillt wird, wird nicht einfach mit dem Rest weitergemacht, als wäre
nichts gewesen, sondern es wird natürlich abgebrochen.
> Und lange vorher schon, wenn "nur" der compiler oder auch der linker> anfaengt zu swappen und genug swap da ist, dann wird der compiler so> dermassen zaeh', dass das auch keiner mehr haben will.
Wenn du einen automatisierten nightly build machst, fällt das unter
Umständen nicht mal auf.
> Was aber wohl leicht passieren kann, ist, dass make der Ansicht ist, man> koennte mehrere compileraufrufe parallelisiert ausfuehren.
Das ist es nur dann, wenn man ihm das explizit sagt, mit dem Parameter
-j.
> Dann laufen z.b. 4x der gcc los, z.b. und beschaeftigen sich mit z.b. 4> Files namens bla1.c, bla2.c bla3.c bla4.c . Die koennten dann verschieden> schnell (z.b. abhaengig von der CPU Last durch andere Prozesse) in> entsprechende .o files verwandelt worden sein. Beim Linkeraufruf koennte> z.b. sowas stehen:> gcc -o bla bla*o
Zumindest die Bash gibt die Ergebnisse der Wildcard-Expansion sortiert
aus. Allerdings kann die Sortierreihenfolge von der locale abhängig
sein. Und bei einer anderen Shell mag auch die Reihenfolge anders sein.
Das könnte man durchaus als "subtile Probleme" durchgehen lassen.
A. K. schrieb:> Von nichtdeterministischer Codeerzeugung in Compilern habe ich> jedenfalls noch nichts gehört.
Was ich schon gehört habe, ist, daß Speicherfehler im normalen Betrieb
oft gar nicht auffallen, aber bei einem Compilerlauf dann zu Problemen
führen. Meistens stürzt der Compiler dann aber ab.
Markus F. schrieb:> -fno-guess-branch-probability/> Do not guess branch probabilities using a randomized model.>> /Sometimes gcc will opt to use a randomized model to guess branch> probabilities, when none are available from either profiling feedback> (-fprofile-arcs) or __builtin_expect. This means that different runs of> the compiler on the same program may produce different object code.
Erstaunlich. Wie soll eine zufallsgesteuerte Branch Prediction denn die
Performance verbessern? Und warum startet der gcc nicht immer mit dem
selben Random Seed? Dann wäre das Ergebnis des selben Codes auch immer
gleich.
Kaj G. schrieb:> Ok, gut zu wissen das ich nicht der Einzige bin, dem das nicht ganz klar> ist :)
ich gehe allerdings davon aus, daß der oben beschriebene
nicht-deterministische Effekt nur auf "high sophisticated"-Plattformen
eine Rolle spielt (und auf simplen Embedded-Controllern eher keine).
Das "Rumprobieren" macht ja nur dort Sinn, wo eine "verschränkte"
Befehlsausführung auch tatsächlich einen Geschwindigkeitsvorteil bringt
(z.B. wenn eine bestimmte CPU aufgrund ihrer Pipelinestruktur irgendwo
noch einen "optimiert eingeschobenen" Maschinenbefehl abarbeiten kann
während sie sonst auf "Nachschub" warten müsste).
Rolf Magnus schrieb:> Erstaunlich. Wie soll eine zufallsgesteuerte Branch Prediction denn die> Performance verbessern?
Mögliche Erklärung: Wenn der Compiler wie üblich streng
deterministischen Code erzeugt, aber bei hochkomplex arbeitenden
Maschinen nicht so recht weiss, ob er damit richtig liegt, dann kann er
am Ende sogar deterministisch falsch liegen. Baut man hingegen einen
Zufallsfaktor ein, dann wird er sehr wahrscheinlich weder am besten noch
am schlechtesten Ende landen, sondern zwischendrin.
Da wir in der Firma auch eine Rebuild-Sicherheit brauchen, hab ich ich
jetzt mal die dafuer zustaendigen Kollegen gefragt, wie wir das hier
machen. Kurz um: das ist eine scheiss Arbeit.
Markus F. schrieb:> Wenn man immer dasselbe Ergebnis haben will, sollte man wohl> -fno-guess-branch-probability spezifizieren.
Ganz so einfach scheint das nicht zu sein, da in das Build-Ergebnis ne
ganze Menge mit einfliesst:
Uhrzeit, Pfad unter dem gebaut wird, Compiler, wer den Compiler wie
gebaut hat, Reihenfolge der Objektdateien, bla bla bla...
Kaj G. schrieb:> Markus F. schrieb:>> Bis vorhin grade hätte ich noch gewettet, daß wenn man den Compiler>> mehrfach auf den selben Quellcode losläßt, auch immer dasselbe>> rauskommt.> Das ist zumindestens das, wo von auch ich bisher ausgegangen bin bzw.> erwartet habe.
Wir machen es so, das immer nur mit einer CPU gebaut wird, es werden
sortierte Dateilisten zum builden genutzt (also nicht *.cpp, sondern:
datei1.cpp datei2.cpp, usw...), und wenn der Build fertig ist, wird das
Binary nochmal gehackt und alles moegliche gestript, wie Dateipfade,
usw.
Einfach nur ein paar Compilerflags setzen scheint auf jedenfall nicht zu
reichen um immer ein bitgleiches Binary zu erhalten.
Hatte mir das gar nicht so schwerwiegend vorgestellt. :-/
Das "Problem" scheint aber nur ab einer gewissen groesse auf zu treten,
denn kleinere Testprogramme kann ich so oft kompilieren wie ich will,
laut
1
md5sum -b <datei>
sind meine Dateien identisch, was bei einem Zeit unterschied von >30
Minuten ja nicht sein duerfte, da ja die Uhrzeit mit einfliesst, etc.
So wirklich greif- und nachvollziehbar sind die im Artikel beschriebenen
Probleme fuer mich nicht :-/
A. K. schrieb:> Von nichtdeterministischer Codeerzeugung in Compilern habe ich> jedenfalls noch nichts gehört. Aber wenn beispielsweise der> Build-Prozess Entscheidungen aufgrund von Laufzeitest trifft, dann kann> das davon abhängen, was sonst noch auf dem Rechner los ist.
Vor vielen Jahren gab's das mal beim Synopsys DesignCompiler. Der hat
den Optimierungsaufwand nach "Wallclock" bestimmt und je nach
Einstellung und Systemauslastung dann früher oder später Feierabend
gemacht.
Kaj G. schrieb:> laut> md5sum -b <datei>> sind meine Dateien identisch, was bei einem Zeit unterschied von >30> Minuten ja nicht sein duerfte, da ja die Uhrzeit mit einfliesst, etc.
Tut sie das?
das häufigste Problem sind Timestamps, die in den verschiedensten
2
Dateiformaten mitgespeichert werden.
Welcher Timerstamp damit jetzt gemeint ist, geht aus dem Artikel leider
nicht hervor.
Was ich aber festgestellt habe:
Alleine das einfuegen einer Leerzeile im Code, veraendert die Checksumme
des Binary, obwohl sich am Code ja nichts geaendert hat. :-/
Der neue Zeitstempel der Datei kann es an der Stelle nicht sein, denn
nehme ich die Leerzeile wieder raus, bekomme ich wieder die alte
Checksumme, obwohl die Datei jetzt wieder einen anderen Zeitstempel hat.
Ausserdem Spielt tatsaechlich der Pfad eine Rolle. Es macht also einen
Unterschied ob man unter
1
/home/user/Docs/
oder
1
/home/user/Docs/projects/my_projects
baut. Gleiches gilt fuer Windows.
Was da so in einer Datei drin steht kann man unter Windows sehr schoen
sehen, wenn man z.B. mit Atmel Studio ein Projekt kompiliert, und dann
z.B. mit dem TotalCommander mal die *.elf Datei anschaut.
Ich hab in dem Artikel aber gerade noch diesen Hinweis gefunden, den ich
vorher wohl uebersehen habe:
1
Ein Skript mit dem Namen strip-nondeterminism entfernt unnötige
2
Informationen aus verschiedenen Dateien.
Jetzt stellt sich mir aber die ernsthafte Frage:
Wenn diese Informationen voellig unnoetig sind, warum zur Hoelle stehen
die dann da ueberhaupt drin? >:-(
Markus F. schrieb:> Bis vorhin grade hätte ich noch gewettet, daß wenn man den Compiler> mehrfach auf den selben Quellcode losläßt, auch immer dasselbe> rauskommt.
Ich denke (mittlerweile) das der eigentliche Code, der rauskommt vom
Prinzip auch nahezu immer der selbe ist (mal abgesehen von diesem
branch-prediction zeug), lediglich die unnuetigen Zusatznformationen
aender sich.
Wenn da so Sachen wie Pfad, Timestamp, Linkreihenfolge, RAM, usw. in das
Ergebnis reinspielen, will ich auch nicht mehr ausschliessen das die
CPU-Auslastung da irgendwie auswirkungen auf das Ergebnis hat.
Scheint irgendwie eine Wissenschaft fuer sich zu sein. :-/
Kaj G. schrieb:> Alleine das einfuegen einer Leerzeile im Code, veraendert die Checksumme> des Binary, obwohl sich am Code ja nichts geaendert hat. :-/> Der neue Zeitstempel der Datei kann es an der Stelle nicht sein, denn> nehme ich die Leerzeile wieder raus, bekomme ich wieder die alte> Checksumme, obwohl die Datei jetzt wieder einen anderen Zeitstempel hat.
Ich denke, es kommt schon drauf an, was genau Du dir anguckst.
Spätestens seit LTO sind in den Objektdateien Segmente drin, die da
eigentlich traditionell nicht reingehören. Der Compiler schreibt darin
seine komplette interne Repräsentation des Programms mit, damit der
Linker sie nochmal zum Optimieren benutzen kann. Dazu muß der Compiler
natürlich irgendwelche Namen "erfinden", die eindeutig und später
wiederzufinden sein müssen. Möglicherweise macht er das aus einer
Prüfsumme des Quellcodes (das würde die Leerzeile erklären).
Wenn diese Informationen vom Linker nicht wieder komplett entfernt
werden, sind natürlich Unterschiede zu finden. Die aber wahrscheinlich
noch nicht einmal irgendeinen Unterschied machen, weil der Loader sie
bei der Ausführung gar nicht mitlädt.
Was da wirklich passiert würde wahrscheinlich klarer, wenn man das
fertige Programm durch objdump einmal in ein "weniger geschwätziges"
Format (srec, beispielsweise) und wieder zurück in ELF (falls man das
braucht) wandelt. Das sollte eigentlich alles Unnötige "abstreifen".
Kaj G. schrieb:> Alleine das einfuegen einer Leerzeile im Code, veraendert die Checksumme> des Binary,
Wenn Leerzeilen und Pfadangaben das Binary verändern, können auch noch
irgendwelche Debuginfos mit drin stecken. Hast du wirklich eine
Release-Version ohne jede Debuginfo gebaut?
Oliver
Oliver S. schrieb:> Wenn Leerzeilen und Pfadangaben das Binary verändern, können auch noch> irgendwelche Debuginfos mit drin stecken. Hast du wirklich eine> Release-Version ohne jede Debuginfo gebaut?
ich glaube der GCC macht das bei Release, dafür gibt es ja dann extra
noch strip was man aufrufen kann.
Oliver S. schrieb:> Kaj G. schrieb:>> Alleine das einfuegen einer Leerzeile im Code, veraendert die Checksumme>> des Binary,>> Wenn Leerzeilen und Pfadangaben das Binary verändern, können auch noch> irgendwelche Debuginfos mit drin stecken. Hast du wirklich eine> Release-Version ohne jede Debuginfo gebaut?
Ja, mein Fehler. Haengt tatsaechlich mit dem Debugflag zusammen. Asche
auf mein Haupt :(
Kaj G. schrieb:> Jetzt stellt sich mir aber die ernsthafte Frage:> Wenn diese Informationen voellig unnoetig sind, warum zur Hoelle stehen> die dann da ueberhaupt drin? >:-(
Kann ja sein, daß sie in manchen Situationen notwendig sind und dann
einfach pauschal automatisch mitgespeichert werden. Oder daß sie an
einem Punkt im build-Prozess notig sind und später nicht mehr, aber eben
mitgeschleift werden, wenn man sie nicht explizit rauswirft.
Wenn sich die hier Mitredenden wirklich für das Thema interessieren
würden, wären sie einfach mal den zwei Links gefolgt, die sie
direktemang zur Auflistung des Debian-Teams geführt hätte, was die
Knackpunkte sind.
Leerzeile in Sourcecode, ich fasse es nicht!
Zwei Beispiele:
* gzip packt per Default einen Zeitstempel mit rein. Kann man
abschalten. (Wer hat denn überhaupt gemerkt, daß es um den gesamten
Buildprozeß ging, nicht nur um einen cc-Aufruf?)
* Unmengen von Debianpaketen (ernsthaft, die sind da alle aufgelistet!)
verwenden in ihren C-Sourcen _DATE__ und __TIME_. Ist natürlich viel
unplausibler als Branch Prediction und sowas, ich weiß.
Freddie schrieb:> Wenn sich die hier Mitredenden wirklich für das Thema interessieren> würden, wären sie einfach mal den zwei Links gefolgt, die sie> direktemang zur Auflistung des Debian-Teams geführt hätte, was die> Knackpunkte sind.
Wenn Du Interessierte für die Debian-Problematik finden willst, wärst Du
vielleicht tatsächlich auf der Debian-Mailingliste besser aufgehoben ;).
Hier interessiert man sich eben dafür, was einen so täglich
interessiert...