Vor einigen Monaten erwähnte ich in einem Beitrag, dass 
dies ein Mythos ist, als ob const dabei hilft, Compiler-Optimierungen in C und C ++ zu ermöglichen . Ich entschied, dass diese Aussage erklärt werden sollte, insbesondere weil ich selbst vorher an diesen Mythos geglaubt hatte. Ich beginne mit Theorie und künstlichen Beispielen und gehe dann zu Experimenten und Benchmarks über, die auf einer echten Codebasis basieren - SQLite.
Einfacher Test
Beginnen wir mit dem einfachsten und offensichtlichsten Beispiel für die Beschleunigung von C-Code mit 
const . Angenommen, wir haben zwei Funktionsdeklarationen:
 void func(int *x); void constFunc(const int *x); 
Angenommen, es gibt zwei Versionen des Codes:
 void byArg(int *x) { printf("%d\n", *x); func(x); printf("%d\n", *x); } void constByArg(const int *x) { printf("%d\n", *x); constFunc(x); printf("%d\n", *x); } 
Um 
printf() auszuführen, muss der Prozessor 
*x über einen Zeiger aus dem Speicher abrufen. Offensichtlich kann die Ausführung von 
constByArg() etwas schneller sein, da der Compiler weiß, dass 
*x eine Konstante ist, sodass der Wert nicht erneut 
constFunc() muss, nachdem 
constFunc() dies getan hat. Richtig? Sehen wir uns den von GCC generierten Assembler-Code mit aktivierten Optimierungen an:
 $ gcc -S -Wall -O3 test.c $ view test.s 
Und hier ist das vollständige Assembler-Ergebnis für 
byArg() :
 byArg: .LFB23: .cfi_startproc pushq %rbx .cfi_def_cfa_offset 16 .cfi_offset 3, -16 movl (%rdi), %edx movq %rdi, %rbx leaq .LC0(%rip), %rsi movl $1, %edi xorl %eax, %eax call __printf_chk@PLT movq %rbx, %rdi call func@PLT # The only instruction that's different in constFoo movl (%rbx), %edx leaq .LC0(%rip), %rsi xorl %eax, %eax movl $1, %edi popq %rbx .cfi_def_cfa_offset 8 jmp __printf_chk@PLT .cfi_endproc 
Der einzige Unterschied zwischen dem für 
byArg() und 
constByArg() generierten Assembler-Code besteht darin, dass 
constByArg() wie im Quellcode einen 
call constFunc@PLT hat. 
const selbst macht keinen Unterschied.
Okay, das war GCC. Vielleicht brauchen wir einen intelligenteren Compiler. Sag Clang.
 $ clang -S -Wall -O3 -emit-llvm test.c $ view test.ll 
Hier ist der Zwischencode. Es ist kompakter als Assembler, und ich werde beide Funktionen löschen, damit Sie verstehen, was ich unter "kein Unterschied, außer dem Aufruf" verstehe:
 ; Function Attrs: nounwind uwtable define dso_local void @byArg(i32*) local_unnamed_addr #0 { %2 = load i32, i32* %0, align 4, !tbaa !2 %3 = tail call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([4 x i8], [4 x i8]* @.str, i64 0, i64 0), i32 %2) tail call void @func(i32* %0) #4 %4 = load i32, i32* %0, align 4, !tbaa !2 %5 = tail call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([4 x i8], [4 x i8]* @.str, i64 0, i64 0), i32 %4) ret void } ; Function Attrs: nounwind uwtable define dso_local void @constByArg(i32*) local_unnamed_addr #0 { %2 = load i32, i32* %0, align 4, !tbaa !2 %3 = tail call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([4 x i8], [4 x i8]* @.str, i64 0, i64 0), i32 %2) tail call void @constFunc(i32* %0) #4 %4 = load i32, i32* %0, align 4, !tbaa !2 %5 = tail call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([4 x i8], [4 x i8]* @.str, i64 0, i64 0), i32 %4) ret void } 
Option, die (Typ) funktioniert
Und hier ist der Code, in dem das Vorhandensein von 
const wirklich wichtig ist:
 void localVar() { int x = 42; printf("%d\n", x); constFunc(&x); printf("%d\n", x); } void constLocalVar() { const int x = 42;  
Der Assembler-Code für 
localVar() , der zwei außerhalb von 
constLocalVar() optimierte Anweisungen enthält:
 localVar: .LFB25: .cfi_startproc subq $24, %rsp .cfi_def_cfa_offset 32 movl $42, %edx movl $1, %edi movq %fs:40, %rax movq %rax, 8(%rsp) xorl %eax, %eax leaq .LC0(%rip), %rsi movl $42, 4(%rsp) call __printf_chk@PLT leaq 4(%rsp), %rdi call constFunc@PLT movl 4(%rsp), %edx # not in constLocalVar() xorl %eax, %eax movl $1, %edi leaq .LC0(%rip), %rsi # not in constLocalVar() call __printf_chk@PLT movq 8(%rsp), %rax xorq %fs:40, %rax jne .L9 addq $24, %rsp .cfi_remember_state .cfi_def_cfa_offset 8 ret .L9: .cfi_restore_state call __stack_chk_fail@PLT .cfi_endproc 
Die LLVM-Middleware ist etwas sauberer. 
load vor dem zweiten Aufruf von 
printf() wurde außerhalb von 
constLocalVar() optimiert:
 ; Function Attrs: nounwind uwtable define dso_local void @localVar() local_unnamed_addr #0 { %1 = alloca i32, align 4 %2 = bitcast i32* %1 to i8* call void @llvm.lifetime.start.p0i8(i64 4, i8* nonnull %2) #4 store i32 42, i32* %1, align 4, !tbaa !2 %3 = tail call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([4 x i8], [4 x i8]* @.str, i64 0, i64 0), i32 42) call void @constFunc(i32* nonnull %1) #4 %4 = load i32, i32* %1, align 4, !tbaa !2 %5 = call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([4 x i8], [4 x i8]* @.str, i64 0, i64 0), i32 %4) call void @llvm.lifetime.end.p0i8(i64 4, i8* nonnull %2) #4 ret void } 
Daher hat 
constLocalVar() den Neustart 
*x erfolgreich ignoriert, aber Sie werden möglicherweise etwas Seltsames bemerken: In den Körpern 
localVar() und 
constLocalVar() derselbe Aufruf von 
constFunc() . Wenn der Compiler herausfinden kann, dass 
constFunc() *x in 
constLocalVar() nicht 
constLocalVar() , warum kann er dann nicht verstehen, dass der gleiche Funktionsaufruf 
*x in 
localVar() nicht 
localVar() ?
Die Erklärung ist, warum die Verwendung von 
const in C als Optimierung unpraktisch ist. In C hat 
const im Wesentlichen zwei mögliche Bedeutungen:
- Dies kann bedeuten, dass eine Variable ein schreibgeschütztes Pseudonym für einige Daten ist, die konstant sein können oder nicht.
 
- oder es kann bedeuten, dass die Variable wirklich eine Konstante ist. Wenn Sie constvon einem Zeiger auf einen konstanten Wert lösen und dann darauf schreiben, erhalten Sie ein undefiniertes Verhalten. Andererseits gibt es kein Problem, wennconstein Zeiger auf einen Wert ist, der keine Konstante ist.
 
Hier ist ein erklärendes Beispiel für die Implementierung von 
constFunc() :
 
localVar() gab 
constFunc() einen 
const Zeiger auf eine nicht- 
const Variable. Da die Variable anfangs nicht 
const , kann sich 
constFunc() als Lügner herausstellen und die Variable zwangsweise ändern, ohne UB zu initiieren. Daher kann der Compiler nicht davon ausgehen, dass 
constFunc() Variable nach der Rückgabe von 
constFunc() denselben Wert hat. Die Variable in 
constLocalVar() Tat 
const , daher kann der Compiler nicht davon ausgehen, dass sie nicht geändert wird, da diesmal UB für 
constFunc() , sodass der Compiler 
const aufhebt und in die Variable schreibt.
Die Funktionen 
byArg() und 
constByArg() aus dem ersten Beispiel sind hoffnungslos, da der Compiler nicht 
constByArg() , ob 
*x const .
Aber woher kam die Inkonsistenz? Wenn der Compiler davon ausgehen kann, dass 
constFunc() sein Argument beim Aufruf von 
constLocalVar() nicht ändert, kann er dieselben Optimierungen auf 
constFunc() -Aufrufe anwenden, oder? Nein. Der Compiler kann nicht davon ausgehen, dass 
constLocalVar() jemals aufgerufen wird. Wenn dies nicht der Fall ist (z. B. weil es sich nur um ein zusätzliches Ergebnis des Codegenerators oder der 
constFunc() kann 
constFunc() die Daten leise ändern, ohne UB zu initiieren.
Möglicherweise müssen Sie die obigen Beispiele und Erklärungen mehrmals lesen. Mach dir keine Sorgen, dass es absurd klingt - es ist. Leider ist das Schreiben in 
const Variablen die schlechteste Art von UB: Meistens weiß der Compiler nicht einmal, ob es sich um UB handelt. Wenn der Compiler 
const sieht, sollte er daher von der Tatsache ausgehen, dass jemand es irgendwo ändern kann, was bedeutet, dass der Compiler 
const zur Optimierung verwenden kann. In der Praxis ist dies der Fall, da viele echte C-Codes eine Ablehnung von 
const im Stil von "Ich weiß, was ich tue" enthalten.
Kurz gesagt, es gibt viele Situationen, in denen der Compiler 
const zur Optimierung verwenden darf, einschließlich des Abrufs von Daten aus einem anderen Bereich mithilfe eines Zeigers oder des Platzierens von Daten auf einem Heap. Oder noch schlimmer, normalerweise in Situationen, in denen der Compiler 
const nicht verwenden kann, ist dies nicht erforderlich. Zum Beispiel kann jeder Compiler mit Selbstachtung ohne 
const verstehen, dass in diesem Code 
x eine Konstante ist:
 int x = 42, y = 0; printf("%d %d\n", x, y); y += x; printf("%d %d\n", x, y); 
Daher ist 
const für die Optimierung fast unbrauchbar, weil:
- Mit wenigen Ausnahmen ist der Compiler gezwungen, dies zu ignorieren, da einige Codes die Konstante legal lösen können.
 
- In den meisten der oben genannten Ausnahmen kann der Compiler immer noch verstehen, dass die Variable eine Konstante ist.
 
C ++
Wenn Sie in C ++ schreiben, kann 
const die Codegenerierung durch Funktionsüberladung beeinflussen. Sie können 
const und non- 
const Überladungen derselben Funktion haben, und non- 
const kann beispielsweise (von einem Programmierer, nicht von einem Compiler) optimiert werden, um weniger zu kopieren.
 void foo(int *p) {  
Einerseits glaube ich nicht, dass dies in der Praxis häufig in C ++ - Code angewendet wird. Andererseits muss ein Programmierer Annahmen treffen, die dem Compiler nicht zur Verfügung stehen, damit sie wirklich einen Unterschied machen, da sie nicht durch die Sprache garantiert werden.
Experimentieren Sie mit SQLite3
Genug Theorie und weit hergeholte Beispiele. Welche Auswirkung hat 
const auf die reale Codebasis? Ich habe mich entschieden, mit SQLite DB (Version 3.30.0) zu experimentieren, weil:
- Es verwendet const.
- Dies ist eine nicht triviale Codebasis (über 200 KLOC).
 
- Als Datenbank enthält sie eine Reihe von Mechanismen, die mit der Verarbeitung von Zeichenfolgenwerten beginnen und mit der Konvertierung von Zahlen in das Datum enden.
 
- Es kann mit einer begrenzten Prozessorlast getestet werden.
 
Darüber hinaus haben der Autor und die an der Entwicklung beteiligten Programmierer bereits Jahre damit verbracht, die Produktivität zu verbessern, sodass wir davon ausgehen können, dass sie nichts Offensichtliches übersehen haben.
Vorbereitung
Ich habe zwei Kopien des 
Quellcodes erstellt . Eine im normalen Modus kompiliert und die zweite mit einem Hack vorverarbeitet, um 
const in einen Leerlaufbefehl umzuwandeln:
 #define const 
(GNU) 
sed kann dies mit dem Befehl 
sed -i '1i#define const' *.c *.h über jede Datei 
sed -i '1i#define const' *.c *.h .
SQLite verkompliziert die Dinge ein wenig und verwendet Skripte, um während des Builds Code zu generieren. Glücklicherweise verursachen Compiler beim Mischen von Code mit 
const und ohne 
const viel Rauschen, sodass Sie die Skripte sofort bemerken und konfigurieren können, um meinen Anti- 
const Code hinzuzufügen.
Ein direkter Vergleich der kompilierten Codes ist nicht sinnvoll, da eine kleine Änderung das gesamte Speicherschema beeinflussen kann, was zu einer Änderung der Zeiger und Funktionsaufrufe im gesamten Code führt. Daher habe ich eine zerlegte Besetzung ( 
objdump -d libSQLite3.so.0.8.6 ) als Größe der Binärdatei und des mnemonischen Namens jeder Anweisung verwendet. Zum Beispiel diese Funktion:
 000000000005d570 <SQLite3_blob_read>: 5d570: 4c 8d 05 59 a2 ff ff lea -0x5da7(%rip),%r8 # 577d0 <SQLite3BtreePayloadChecked> 5d577: e9 04 fe ff ff jmpq 5d380 <blobReadWrite> 5d57c: 0f 1f 40 00 nopl 0x0(%rax) 
Wird zu:
 SQLite3_blob_read 7lea 5jmpq 4nopl 
Beim Kompilieren habe ich die SQLite-Assemblyeinstellungen nicht geändert.
Kompilierte Code-Analyse
Für libSQLite3.so belegte die Version mit 
const 4.740.704 Bytes, ungefähr 0,1% mehr als die Version ohne 
const mit 4.736.712 Bytes. In beiden Fällen wurden 1374 Funktionen exportiert (ohne die Hilfsfunktionen auf niedriger Ebene im PLT), und 13 wiesen Unterschiede in den Besetzungen auf.
Einige Änderungen betrafen den Vorverarbeitungs-Hack. Hier ist zum Beispiel eine der geänderten Funktionen (ich habe einige für SQLite spezifische Definitionen entfernt):
 #define LARGEST_INT64 (0xffffffff|(((int64_t)0x7fffffff)<<32)) #define SMALLEST_INT64 (((int64_t)-1) - LARGEST_INT64) static int64_t doubleToInt64(double r){ /* ** Many compilers we encounter do not define constants for the ** minimum and maximum 64-bit integers, or they define them ** inconsistently. And many do not understand the "LL" notation. ** So we define our own static constants here using nothing ** larger than a 32-bit integer constant. */ static const int64_t maxInt = LARGEST_INT64; static const int64_t minInt = SMALLEST_INT64; if( r<=(double)minInt ){ return minInt; }else if( r>=(double)maxInt ){ return maxInt; }else{ return (int64_t)r; } } 
Wenn wir 
const entfernen, werden diese Konstanten zu 
static Variablen. Ich verstehe nicht, warum jeder, der sich nicht für 
const interessiert, diese Variablen 
static machen 
const . Wenn wir sowohl 
static als auch 
const entfernen, betrachtet GCC sie erneut als Konstanten und wir erhalten das gleiche Ergebnis. Aufgrund solcher 
static const Variablen erwiesen sich Änderungen in drei von dreizehn Funktionen als falsch, aber ich habe sie nicht behoben.
SQLite verwendet viele globale Variablen, und die meisten echten Konstantenoptimierungen hängen damit zusammen: Wie das Ersetzen eines Vergleichs durch eine Variable durch einen Vergleich mit einer Konstanten oder das teilweise Zurückrollen der Schleife um einen Schritt (um zu verstehen, welche Art von Optimierungen vorgenommen wurden, habe ich 
Radare verwendet ). Einige Änderungen sind nicht erwähnenswert. 
SQLite3ParseUri() enthält 487 Anweisungen, aber 
const hat nur eine Änderung vorgenommen: Diese beiden Vergleiche wurden durchgeführt:
 test %al, %al je <SQLite3ParseUri+0x717> cmp $0x23, %al je <SQLite3ParseUri+0x717> 
Und getauscht:
 cmp $0x23, %al je <SQLite3ParseUri+0x717> test %al, %al je <SQLite3ParseUri+0x717> 
Benchmarks
SQLite wird mit einem Regressionstest geliefert, um die Leistung zu messen. Ich habe ihn hunderte Male für jede Version des Codes unter Verwendung der Standard-SQLite-Build-Einstellungen ausgeführt. Ausführungszeit in Sekunden:
Persönlich sehe ich keinen großen Unterschied. Ich habe 
const aus dem gesamten Programm entfernt. Wenn es also einen merklichen Unterschied gab, war es leicht zu bemerken. Wenn Ihnen die Leistung jedoch extrem wichtig ist, kann Ihnen auch eine winzige Beschleunigung gefallen. Lassen Sie uns eine statistische Analyse durchführen.
Ich verwende gerne den Mann-Whitney-U-Test für solche Aufgaben. Er ähnelt dem bekannteren t-Test, mit dem Unterschiede in Gruppen ermittelt werden sollen, ist jedoch widerstandsfähiger gegen komplexe zufällige Abweichungen, die bei der Zeitmessung auf Computern auftreten (aufgrund unvorhersehbarer Kontextwechsel, Fehler in) Gedächtnisseiten usw.). Hier ist das Ergebnis:
Test U ergab einen statistisch signifikanten Leistungsunterschied. Aber - eine Überraschung! - Die Version ohne 
const erwies sich um etwa 60 ms, dh um 0,5%, als schneller. Es scheint, dass die geringe Anzahl der vorgenommenen „Optimierungen“ die Erhöhung der Codemenge nicht wert war. Es ist unwahrscheinlich, dass 
const wichtige Optimierungen wie die automatische Vektorisierung aktiviert 
const . Natürlich kann Ihr Kilometerstand von verschiedenen Flags im Compiler oder von seiner Version oder von der Codebasis oder von etwas anderem abhängen. Aber es scheint mir ehrlich zu sein, dass ich dies nicht bemerkt habe, auch wenn 
const die Leistung von C verbessert hat.
Wofür wird const benötigt?
Trotz aller Mängel ist 
const in C / C ++ nützlich, um die Typensicherheit zu gewährleisten. Insbesondere wenn Sie 
const in Kombination mit Verschiebungssemantik und 
std::unique_pointer , können Sie den expliziten Zeigerbesitz implementieren. Die Unsicherheit des Zeigerbesitzes war ein großes Problem in älteren C ++ - Codebasen über 100 KLOC, daher bin ich 
const dankbar, dass er es gelöst hat.
Bevor ich jedoch über die Verwendung von 
const hinausging, um die 
const zu gewährleisten. Ich habe gehört, dass es als richtig angesehen wurde, 
const so aktiv wie möglich zu verwenden, um die Leistung zu verbessern. Ich habe gehört, dass man, wenn die Leistung wirklich wichtig ist, den Code umgestalten musste, um mehr 
const hinzuzufügen, selbst wenn der Code weniger lesbar wurde. Es klang damals vernünftig, aber seitdem wurde mir klar, dass dies nicht stimmte.