(das wird jetzt OT, aber ich hatte gerade Lust dazu...)
Ist `is` schneller als `==`? Generell ja, aber die beiden Operationen haben verschiedene Aufgaben und es wäre, als wenn man fragen würde, ob Addition wohl schneller ist als Multiplikation (was in der Regel auch zutrifft).
Warum werden bei CPython bestimmte Zahlenobjekte vorinitialisiert? Das ist eine Performance-Optimierung aufgrund der konkreten Objektrepräsentation von CPython. Ich schreibe extra CPython, da dies eine Design-Entscheidung dieses konkreten Interpreters ist und nicht eine Eigenschaft der Sprache Python.
Wenn man Python-Objekte im Speicher repräsentieren will, wählt man meist den "tagged values"-Ansatz, d.h. jedem Stück Speicher, in dem die Bit-Repräsentation des Objekts steht, ist ein "tag", eine Markierung vorausgestellt, die angibt, was die Bits bedeuten. So könnte eine 1 für "int", eine 2 für "str", eine 3 für "type" usw. stehen. Eine alternative Repräsentation wären tagged pointers, aber AFAIK nutzt die kaum eine Sprache, auch wenn das bei 64-bit-Maschinen möglicherweise effizienter wäre. Bei 32-bit-Maschinen sind jedoch tagged values besser.
In C könnte das (mein C ist eingerostet, daher lege ich keinen Finger ins Feuer) so aussehen:
Code: Alles auswählen
struct tagged_val {
int tag;
union {
int i;
char s[0];
...
} val;
};
typedef struct tagged_val *VALUE;
Will ich nun eine Zahl "42" anlegen, hilft mir die folgende Funktion, den für "tag" und "val" notwendigen Speicher zu reservieren und zu initialisieren und liefert mit ein "VALUE" - also einen Zeiger auf den Speicherbereich zurück.
Code: Alles auswählen
VALUE make_int(int i) {
VALUE v = alloc(sizeof(int) + sizeof(int));
v->tag = 1;
v->val.i = i;
return v;
}
Für Strings funktioniert das analog.
Code: Alles auswählen
VALUE make_str(char *s) {
VALUE v = alloc(sizeof(int) + sizeof(char) * (strlen(s) + 1));
v->tag = 2;
strcpy(&v->val.s, s);
return v;
}
Will ich prüfen, ob ein "VALUE" ein "int" oder ein "str" ist, kann ich dafür passende Funktionen in C zur Verfügnug stellen:
Code: Alles auswählen
int is_int(VALUE v) {
return v->tag == 1;
}
int is_str(VALUE v) {
return v->tag == 2;
}
Nun kann ich zwei Zahlen addieren, indem ich sicherstelle, dass es überhaupt Objekte vom Typ "int" sind, dann deren "val" auslese, in C die Addition auf C-"int" durchführe und dann wieder ein "int"-VALUE erzeuge.
Code: Alles auswählen
VALUE add(VALUE a, VALUE b) {
assert(is_int(a) && is_int(b));
return make_int(a->val.i + b->val.i);
}
Man erkennt, dass jetzt alle Rechenoperationen mit neuen Zahlenobjekten nur so um sich schmeißen und deren Speicher muss verwaltet werden. Ein simpler Garbage-Collection-Algorithmus (oder das noch primitivere Referenzenzählen von CPython) sind damit schnell überfordert und die Performance des Systems bricht ein. Daher hält CPython für die häufig verwendeten Zahlen die Objekte schon vor, um nicht jedes Mal deren Speicher neu anzufordern, Referenzen zu zählen und den Speicher dann wieder freizugeben.
Eine typische Optimierung, die CPython nicht macht, wäre bestimmte häufig vorkommende Objekttypen wie z.B. "int" durch sogenannte "immediate values" zu repräsentieren, dass sind verkappte Zeiger, die auf gar nichts zeigen, sondern wo die Bitrepräsentation des Zeigers als Wert interpretiert wird. Dabei wird ausgenutzt, dass Zeiger meist nur auf gerade Speicheradressen oder auf Adressen, die durch 4 oder 8 teilbar sind zeigen. Das dies immer zutrifft, kann man in "alloc" zusichern. Wenn Adressen z.B immer gerade sind, ist das unterste Bit immer 0. Trifft der Interpreter auf einen Zeiger, dessen unterstes Bit gesetzt ist, kann eine Sonderbehandlung stattfinden und die restlichen 31 oder 63 Bits können als Integer-Zahl interpretiert werden.
Das sieht dann so in C aus:
Code: Alles auswählen
typedef void *VALUE;
VALUE make_int(int i) {
return (VALUE) i << 1 + 1;
}
VALUE make_str(char *s) {
struct tagged_val *tv = alloc(sizeof(int) + sizeof(char) * (strlen(s) + 1));
tv->tag = 1;
strcpy(&tv->val.s, s);
return (VALUE) tv;
}
int is_int(VALUE v) {
return ((int) v) & 1;
}
int is_str(VALUE v) {
return !is_int(v) && ((struct tagged_val *) v)->tag == 1;
}
Nun kann man mit Zahlen deutlich effizienter operieren und sie verbrauchen niemals Speicher, der mit "alloc" reserviert und dann vom Garbage Collector verwaltet werden muss.
Code: Alles auswählen
VALUE add(VALUE a, VALUE b) {
assert(is_int(a) && is_int(b));
return (VALUE) (((int) a) - 1 + ((int) b));
}
Noch besser wäre, wenn man Zahlen nicht mit gesetztem untersten Bit repräsentiert, sondern anders herum, bei Zahlen das unterste Bit gelöscht hat und bei allen Zeigern dafür das unterste Bit setzt. Bei den alten SPARC-Prozessoren war das AFAIK egal, dort mussten sowieso die Adressen durch 4 teilbar sein und die CPU hat die untersten beiden Bit daher ignoriert - genau um dort derartige eingebettete "tags" eintragen zu können. Das hat frühe Lisp-Systeme auf SPARC deutlich effizienter gemacht als auf dem 6800er (Intel hatte zu der Zeit noch eine nennenswerten CPUs im Angebot). Inzwischen ist alles Intel-dominiert und ich weiß nicht, ob dort irgendwelche Tricks funktionieren. Hinzu kommt, dass man Interpreter für Sprachen nicht mehr in Assembler schreibt, sondern in C (oder in einer Hochsprache) und gewisse Low-Level-Operationen noch nicht einmal portabel in C beschreibbar sind und man daher in der Regel darauf verzichtet.
Reserviert man eines von 32 Bits für ein "immediate"-Flag, können Zahlen natürlich nur noch 31 Bits für ihre Repräsentation haben und man kann nur noch -1073741824 bis +1073741823 effizient darstellen. Nutzt man 2 bits, weil man z.B. Wahrheitswerte, Buchstaben, Symbole, Cons-Zellen oder andere häufige Datentypen auch noch effizient darstellen will, schrumpft der Zahlenbereich weiter. Ich vermute, daher hat man sich bei Python gegen diese Repräsentation entschieden. Zur Geburtsstunde der Sprache hatten die Maschinen ja eher 16-Bit-CPUs, sodass die Zahlen richtig klein geworden wären.
Im Zeitalter von 64-Bit denke ich allerdings, dass man sich locker 8 oder 16 ganz oben in den Adressen als tag gönnen könnte, weil auch 48-Bit noch einen ausreichend großen Adressraum beschreiben würden.
Stefan