&-Operator in diesem Zusammenhang

wolfgang12

Mitglied
Hallo,

ich verstehe hier in diesem Code-Schnippsel den "UND-Operator" nicht ('&').

Java:
static int isLastBitSet(int i){
		switch  ( i & 1 ){ // --> Was macht hier das '&'
		case 0: return 0;
		case 1: return 1;
		default: return 2;  //Nur weil das Prog sonst nicht kompiliert werden kann
		}
		
	}


Ich weiß, dass per UND-Operator ausdrücke logisch verknüpft werden können. Bei und müssen halt beide "Seiten" true ergeben damit auch das ergebnis true ist. Aber hier wird ja nichts ausgewertet. Das verstehe ich nicht. ;(



Vielen Dank 😀
 
Mit booleschen Ausdrücken hat das
Code:
&
hier nichts zu tun. Es ist ein Bit-Operator, der von deinem
Code:
int i
das letzte Bit umkippen lässt (im Binärsystem, also 1 wird zu 0 und 0 wird zu 1).

Ich bin mir nicht ganz sicher, da ich das selbst erst vor kurzem kennen gelernt habe, deshalb hoffe ich, jetzt nicht allzu viel Mist verzapft zu haben…
 
[EDIT]falsch gelesen - teil gelöscht[/EDIT]
0&egal ist immer 0
Du hast z.B. Zahlen
01010101111101 &
00000000000001
Alle bis auf die 1. Stelle werden automatisch 0. Wenn die 1. Stelle von deinem i 0 ist, dann ist das Resultat 0, ansonsten ist das Resultat 1
 
ist java in dem fall so schlau und prüft wirklich nur das letzte bit, oder macht es unsinnigerweise noch den check auf dem rest von i, was ja unnötig wäre?!
 
ist java in dem fall so schlau und prüft wirklich nur das letzte bit, oder macht es unsinnigerweise noch den check auf dem rest von i, was ja unnötig wäre?!
Ich versteh deine Frage nicht ganz. Java überprüft da gar nichts. Das macht die ALU in deiner CPU. Und wie die das macht musst du den Fragen, der die CPU entwickelt hat...
 
Hallo,

ich verstehe hier in diesem Code-Schnippsel den "UND-Operator" nicht ('&').

Java:
static int isLastBitSet(int i){
		switch  ( i & 1 ){ // --> Was macht hier das '&'
		case 0: return 0;
		case 1: return 1;
		default: return 2;  //Nur weil das Prog sonst nicht kompiliert werden kann
		}
		
	}
Die Methode macht, was ihr Name vermuten lässt: Gibt true zurück, wenn das letzte Bit bei i gesetzt ist. Aber da ich dieses grausame switch-Statement nicht sehen kann, hier nochmal in einer Zeite, "ordentlich" ^^:

Java:
public static boolean isLastBitSet(int i) {
    return (i & 1) == 1;
}
Finde ich lesbarer und eindeutiger. 🙂
 
Anmerkung:
Ein Bit kann nur eine 1 oder eine 0 sein. Desshalb ist eine Switch-Anweisung eigentlich fehl am Platz. Du solltest lieber eine If-Abfrage benutzten. Bzw. die Lösung von Galileo Computing ist auch nicht schlecht.
 
ist java in dem fall so schlau und prüft wirklich nur das letzte bit, oder macht es unsinnigerweise noch den check auf dem rest von i, was ja unnötig wäre?!
Definiere "Rest". Warum sollte denn bei & etwas komplizierter kompiliert werden, als es ist? Welchen Teil von & sollte denn die VM (und noch wichtiger: die "echte" CPU) nicht verstehen?

Deine darauffolgende Fragestellung hat damit aber wahrscheinlich nichts mehr zu tun. Präzisiere dich!

Ark
 
Definiere "Rest". Warum sollte denn bei & etwas komplizierter kompiliert werden, als es ist? Welchen Teil von & sollte denn die VM (und noch wichtiger: die "echte" CPU) nicht verstehen?

Deine darauffolgende Fragestellung hat damit aber wahrscheinlich nichts mehr zu tun. Präzisiere dich!

Ark



es geht mir darum:
i = 11000100001010101

wenn ich nun i & 1 auswerten lasse wäre es ja unsinnig überhaupt die anderen Stellen mit Ausnahme des ersten Bits zu prüfen. Bei ner dynamischen Variable i, welche den Inhalt zur Laufzeit ändert, kann der Compiler nicht zaubern. Aber wenn i bereits zur Compilezeit feststeht, wird da vielleicht intelligenter agiert?
Es ist eh ein unerheblicher Teil an Zeit, welchen man da einsparen könnte, aber die Frage schoss mir durch den Kopf und ich dacht, vielleicht kennt jemand die Verfahrensweise 🙂
 
Java prüft überhaupt nichts. Das ist eine Operation wie Addieren und Subtrahieren. Entweder habe ich deine Frage falsch verstanden oder du weist nicht genau wie Bitweisen-Operatoren funktionieren.

Code:
 101010101010101 -> Irendeine Zahl
&000000000000001 -> Entspricht int i = 1
 000000000000001 -> Enstpricht int i = 1 (Ergebnis) (ist aber kein boolischer Wert)

Das wäre so als würde folgender Quelltext dort stehen:

switch(i - 244)
 
Zuletzt bearbeitet:
Anders gesagt: Der Prozessor schafft [c]a & b[/c] in einem Prozessorzyklus, egal was in a oder b drinsteht (ok kommt evtl auf den Prozessor an, ich weiß es nicht 100%, aber ich denk man kann es sich so vorstellen... 😉).

Wenn beide Variablen schon zur Compile-Zeit feststehen, dann kann es sein dass der Compiler gleich schon das Ergebnis hinschreibt. Aber das ist wohl eher nicht der Normalfall...
 
Wenn beide Variablen schon zur Compile-Zeit feststehen, dann kann es sein dass der Compiler gleich schon das Ergebnis hinschreibt. Aber das ist wohl eher nicht der Normalfall...
Ich hätte jetzt genau das Gegenteil behauptet. Wenn zur Compilezeit schon festgestellt werden kann, dass eine Rechnung unnötig ist, warum sollte der Compiler sie dann nicht durch das Ergebnis ersetzen?
Ich hab das mal beim GCC ausprobiert. Der hat tatsächlich alle Berechnungen, die auf statischen Variablen beruhten wegoptimiert. Im Asm-Code wurden nur noch die Ergebnisse der Rechnung in die Register geladen, die Rechnung selber wurde aber nicht mehr ausgeführt. Ich hab aber keine Ahnung in wie weit der JIT-Compiler der JVM da optimiert.
 
Ich hätte jetzt genau das Gegenteil behauptet. Wenn zur Compilezeit schon festgestellt werden kann, dass eine Rechnung unnötig ist, warum sollte der Compiler sie dann nicht durch das Ergebnis ersetzen?
Ich hab das mal beim GCC ausprobiert. Der hat tatsächlich alle Berechnungen, die auf statischen Variablen beruhten wegoptimiert. Im Asm-Code wurden nur noch die Ergebnisse der Rechnung in die Register geladen, die Rechnung selber wurde aber nicht mehr ausgeführt. Ich hab aber keine Ahnung in wie weit der JIT-Compiler der JVM da optimiert.

Ups, ich meinte: dass beide Variablen schon feststehen ist eher nicht der Normalfall. 😉
 
@diggaa1984: Also, das ist so:

Der Bytecode-Compiler (das Ding, das aus java-Dateien class-Dateien macht) kompiliert für die Java VM. Diese virtuelle Maschine hat einen virtuellen Prozessor, so wie auch jede reale Maschine einen realen Prozessor hat. Und so ein Prozessor (egal ob real oder virtuell) hat einen bestimmten Befehlssatz. Gerade dieser Befehlssatz ist ein sehr wesentliches Unterscheidungsmerkmal bei Prozessoren.

Java erreicht die Plattformunabhängigkeit unter anderem dadurch, dass eben nicht für einen real existierenden Prozessor kompiliert wird, sondern nur für einen virtuellen (nämlich den der VM). Ein Compiler muss sich immer auf eine bestimmte Zielsprache einstellen bzw. ist allein dafür geschaffen worden, Quelltexte einer ganz bestimmten Sprache in die Sprache eines bestimmten Prozessors zu übersetzen.

Da der Bytecode-Compiler als Zielsprache die des virtuellen Prozessors hat, kann er auch nur in Befehle übersetzen, die für diesen virtuellen Prozessor verfügbar sind. Ob da Befehle zum Überprüfen einzelner Bits zur Verfügung stehen, kann ich gerade nicht sagen, halte ich aber für eher unwahrscheinlich. Das muss aber noch gar nicht so viel sagen.

Denn zur Laufzeit muss noch ein JIT-Compiler oder Interpreter ran. Der Interpreter ist im Wesentlichen eine Schleife, die entsprechend dem aktuellen Befehlscode ein Stückchen nativen Code aufruft, der dann ausgeführt wird. Der JIT-Compiler erkennt genauso die Befehle wie der Interpreter, führt sie aber nicht sofort aus, sondern schreibt sie sich vielmehr erst einmal auf. Wenn dann der Compiler genügend Code aufgeschrieben hat, ist er möglicherweise in der Lage, größere Zusammenhänge zwischen kleinen Codestückchen zu finden, und kann dann Optimierungen vornehmen.

Aber auch hier ist wieder einmal der Compiler an die Zielsprache gebunden. Wenn also der Befehlssatz des realen Prozessors einen Befehl kennt, um einzelne Bits zu prüfen, dann kann(!) der Compiler eventuell auf die Idee kommen, tatsächlich auch diesen Befehl dann, wenn es angebracht ist, einzusetzen.

Solche Optimierungen zur Laufzeit kosten zunächst einmal Zeit, denn es muss erst einmal kompiliert werden (vom Befehlssatz des virtuellen Prozessors zum Befehlssatz des realen Prozessors). Das erhöht die Latenz, aber auch die Geschwindigkeit.

Der Bytecode-Compiler erzeugt Code für einen virtuellen Prozessor, dessen Arbeitsweise eher an Stackmaschinen erinnert. Der JIT-Compiler dagegen muss diesen Bytecode in die Maschinensprache des realen Prozessors übersetzen. Dieser reale Prozessor erinnert eher an Registermaschinen. Ein wesentlicher Teil der Zeit wird damit verbracht, die Daten, die sonst auf dem Stack liegen, Registern zuzuweisen. Gerade hier kann der JIT-Compiler viel verbessern (um nicht zu sagen optimieren), und da ist es schon eine sehr hohe Kunst, ganz spezielle Befehle des realen Prozessors zu finden, die an dieser Stelle genutzt werden können. (Immerhin soll das Programm ja schnell kompiliert sein, und da muss der JIT-Compiler irgendwann fertig werden bzw. mit Optimieren aufhöhren.) Insofern kann es hier sehr auf die JIT-Compiler ankommen, ob so ein spezieller Befehl benutzt werden wird oder nicht.

Kurzum: Ob spezielle Befehle genutzt werden (können), hängt von sehr, sehr vielen Dingen ab. 😉

Ark
 
wenn ich nun i & 1 auswerten lasse wäre es ja unsinnig überhaupt die anderen Stellen mit Ausnahme des ersten Bits zu prüfen
Ich vermute, es wird ein Register mit einem anderen verundet, und das geschieht für alle Bits eines Wortes parallel in einem Takt.

Wie lange Java benötigt und was genau passiert, um zu prüfen, ob das Ergebnis eine 0, eine 1 oder etwas anderes ist, ist für mich so unwichtig, daß mich nicht einmal die Antwort interessiert. Das Weichenkonstrukt ist, wie bereits Jay_030 schrieb, ohnehin an dieser Stelle mißbraucht; und von einer Methode solch einem Namen erwarte ich einen Wahrheitswert, keine Ganzzahl. So, wie sie ist, kann man sie jedenfalls nicht ordentlich verwenden.
 
Zuletzt bearbeitet:
Ich vermute, es wird ein Register mit einem anderen verundet, und das geschieht für alle Bits eines Wortes parallel in einem Takt.
Das sehe ich genauso.

Wie lange Java benötigt und was genau passiert, um zu prüfen, ob das Ergebnis eine 0, eine 1 oder etwas anderes ist, ist für mich so unwichtig, daß mich nicht einmal die Antwort interessiert.
An dieser Stelle bin ich definitiv anderer Meinung. Sowohl bei switches als auch bei solchen arithmetisch-logischen Ausdrücken kann man die Compiler unterstützen.

Bei switches ist es wichtig, die cases (die Werte, die der Ausdruck zum Wort [c]switch[/c] annehmen kann/muss) streng monoton steigend und möglichst geschlossen zu wählen. Wenn kein fall-through ausgenutzt wird, kann der Bytecode-Compiler die alternativen Codeteile umsortieren, um sie streng monoton steigend hinzubekommen, sonst nicht. (Ob der Compiler das macht, ist noch mal eine andere Frage.) Wenn die cases dicht genug beieinander liegen, kann eine Sprungtabelle angelegt werden, mit der dann praktisch garantiert werden kann, dass jeder Einsprung die gleiche Anzahl an Schritten benötigt; im Allgemeinen ist solcher Code auch schneller. Wenn die cases jedoch zu weit auseinanderliegen oder (trotz Umstellversuche durch den Bytecode-Compiler) nicht streng monoton steigend vorliegen, wird keine Sprungtabelle mehr eingesetzt, sondern auf ein klassisches if-else-Konstrukt gesetzt. Das kann dann dazu führen, dass weiter hinten liegende Alternativen immer später drankommen (also mit größerer Verzögerung) als frühere, und insgesamt wird die Ausführung sehr wahrscheinlich langsamer sein.

(Eventuell kann der Compiler auch bei streng monoton fallenden cases Sprungtabellen einsetzen.)

Bei arithmetisch-logischen Ausdrücken kann man zum einen die Anzahl der Operationen selbst verringern (oder sie eventuell durch schnellere austauschen), zum anderen kann man auf Ausdrücke setzen, deren Werte implizit berechnet werden. So muss bei [c](a & 1) == 1[/c] der Vergleich mit der 1 explizit durchgeführt werden, und das kostet für die Konstante 1 ein wenig Zeit und immerhin ein wertvolles Register, das man anderweitig besser nutzen könnte. Schreibt man dagegen [c](a & 1) != 0[/c], so kann der Vergleich mit 0 komplett entfallen, da das Zero-Flag bei praktisch allen Prozessoren automatisch bei der UND-Verknüpfung entsprechend gesetzt wird.

Das Weichenkonstrukt ist, wie bereits Jay_030 schrieb, ohnehin an dieser Stelle mißbraucht; und von einer Methode solch einem Namen erwarte ich einen Wahrheitswert, keine Ganzzahl. So, wie sie ist, kann man sie jedenfalls nicht ordentlich verwenden.
Dem stimme ich wieder zu.

Ark
 
Bei arithmetisch-logischen Ausdrücken kann man zum einen die Anzahl der Operationen selbst verringern (oder sie eventuell durch schnellere austauschen), zum anderen kann man auf Ausdrücke setzen, deren Werte implizit berechnet werden. So muss bei [c](a & 1) == 1[/c] der Vergleich mit der 1 explizit durchgeführt werden, und das kostet für die Konstante 1 ein wenig Zeit und immerhin ein wertvolles Register, das man anderweitig besser nutzen könnte. Schreibt man dagegen [c](a & 1) != 0[/c], so kann der Vergleich mit 0 komplett entfallen, da das Zero-Flag bei praktisch allen Prozessoren automatisch bei der UND-Verknüpfung entsprechend gesetzt wird.
Wieder etwas gelernt. Danke. 🙂

Das kann der Compiler ja nur schlecht selbst wegoptimieren, denn zwischenn == 1 und != 0 ist semantisch aus Sicht der Compilers ein großer Unterschied.

Wie gesagt, danke für deine Erklärungen.
 

Neue Themen


Zurück
Oben