Forum: Compiler & IDEs (No)-Aliasing for fun and performance


von Wilhelm M. (wimalopaan)


Angehängte Dateien:

Lesenswert?

Aliasing oder warum es (fast immer) falsch ist, mehrstellige freie 
Funktionen mit primitiven Datentypen zu verwenden oder der Teufel steckt 
auch im char.

Wir alle wollen Performance. Und dafür ist die strict-aliasing-rule 
gemacht.
Was ist das?
Ganz kurz: haben zwei Zeiger oder Referenzen einen unterschiedlichen 
(Basis)-Typ, dann müssen sie
auf unterschiedlich Objekt verweisen. Denn an einer Stelle im Speicher 
kann nur ein Objekt mit einem
eindeutigen Typ  sein.

Umgekehrt dann: haben zwei Zeiger oder Referenz denselben (Basis)-Typ, 
so können Sie auf dasselbe Objekt verweisen.

Dies muss der Compiler bei seinen Optimierungen berücksichtugen. Und er 
tut das auch. Ja, solange der Programmierer
den Compiler nicht betrügt (s.a. reinterpret_cast) oder anderweitig UB 
einbaut (unions).

Hat man eine Signatur wie etwa (kein Aliasing möglich)
1
void foo1(int&, short&);

so kann der Compiler besser optimieren als bei (Aliasing möglich)
1
void foo2(int&, int&);

Ich denke, das wissen alle/die meisten hier.

Etwas weniger bekannt dürfte sein, dass der Compiler auch in den 
folgenden
beiden Fällen die strict-aliasing rule nicht(!) anwenden kann.
1
void foo3(uint8_t&, int&);
2
void foo4(char*,    int&);

Bei foo3(uint8_t&, int&) oder foo4(char*, int&) kann der Compiler 
ebenfalls nicht annehmen, das
die referenzierten Objekte unterschiedlich sind. Denn der Datentyp char 
wie auch uint8_t und std::byte
kann ein alias für alles(!) sein. Dies ist eine spezielle Regel, damit 
man (std::memcpy) an
die Objektrepräsentation überhaupt (ohne UB) heran kommt. Für diese 
Funktionen muss also der weniger
gute Code generiert werden.

Damit also der Compiler korrekt optimieren kann, ist es besser

1) in mehrstelligen freien Funktion unterschiedlich (Referenz-) Typen zu 
verwenden, wobei
2) diese Typen weder aliase für char (uint8_t und andere Typaliase 
dafür) noch std::byte sein dürfen.

Das Ganze fügt sich in gut ein, denn

i) char als Byte-Typ zu verwenden ist falsch, weil ein Byte nur eine 
Ansammlung von Bits ist,
und bspw. die arithmetischen Operationen dafür keinen Sinn machen.

ii) char als Datentyp für ein Zeichen (UTF-8, ASCII) ist ebenfalls 
falsch, da sonst der Compiler immer
vom Aliasing-Fall ausgehen muss.

Also:

i) Als Byte-Type std::byte verwenden, damit die Qualität der Daten 
korrekt modelliert ist,
und man aber auch Aliasing erhält.

ii) Als Zeichendatentyp char8_t oder vergleichbares verwenden, damit 
auch hier die unsinngen Operationen
unmöglich sind, und aber auch kein Aliasing angenommen werden muss.

iii) Auch für alle anderen primitiven Datentypen sog. StrongType's 
verwenden (ggf. als template).

iv) Bei char/std::byte Arrays eben auch std::array oder entsprechende 
StrongTypes verwenden.

v) Nur wenn man Aliasing (und damit weniger guten Code) zulassen möchte, 
sollte man char et.al. verwenden.

Mein Merkspruch dazu, den ich ja schon desöfteren gepostet habe:

Die primitiven Datentypen sind dazu dar, nicht(!) benutzt zu werden.

Dies führt fast unmittelbar dazu, dass man (fast) allen Code als 
template schreibt.

Dies bringt noch weitere Vorteile wie inlining, header-only ohne 
Architekturcompilate, Flexibilität, ... mit sich,
was sich zusätzlich positiv auf die Performance auswirken kann.

Nun mag man einwenden: ich verwende doch gar keine mehrstellingen freien 
Funktionen mit denselben Typen
als Outputparameter in der Parameterliste.

Nun, es reicht auch ein Input- und ein Output-Parameter bei einer freien 
Funktion, oder eben eine Elementfunktion mit einem
Inputparameter:
1
struct X {
2
    uint8_t test1(const X& o) { // Aliasing möglich
3
        value += o.value;
4
        return o.value;
5
    }    
6
    uint8_t value{};
7
};

Und natürlich auch wieder der Typ char (uint8_t, std::byte):
1
struct X {
2
    uint8_t test2(const char* o) { // Aliasing möglich: char* ist alias fuer alles
3
        value += *o;
4
        return *o;
5
    }    
6
    uint8_t value{};
7
};

Zwar ist eine Struktur wie in X::test1() eher selten. Bei einem 
Output-Parameter in einer Elementfunktion
kann das aber wieder ganz schnell auftreten. Und eben keine Referenzen 
für die all-alias-typen char (und Typaliase dafür)
und std::byte verwenden.

Für die Spielfreudigen:

https://gcc.godbolt.org/z/QMQW2c

Oder die Datei im Anhang verwenden.

von Heiko L. (zer0)


Lesenswert?

Wilhelm M. schrieb:
> iii) Auch für alle anderen primitiven Datentypen sog. StrongType's
> verwenden (ggf. als template).

Das nützt dir in Bezug auf Aliasing wenig, solange die Typen 
pointer-interconvertible sind.
Du müsstest noch die "StandardLayout"-property aushebeln, denke ich.

Irgendwie so, vielleicht:
1
template<typename T, typename Tag>
2
struct X {
3
private: T x;
4
public: [[no_unique_address]] Tag tag___;
5
6
...
7
};

Edit: Und auch dann wäre ich mir nicht sicher, dass nicht final gilt 
"ein int ist ein int".

: Bearbeitet durch User
von Guest (Gast)


Lesenswert?

Ältere Microsoft-Compiler hatten die Option
"assume no aliasing across function calls" - noch einigermaßen sicher

und

"assume no aliasing" - beste Performance aber kaum ein Programm lief 
fehlerfrei ;-)

von Heiko L. (zer0)


Lesenswert?

Wilhelm M. schrieb:
> Dies bringt noch weitere Vorteile wie inlining, header-only ohne
> Architekturcompilate, Flexibilität, ... mit sich,
> was sich zusätzlich positiv auf die Performance auswirken kann.

Wobei das auch massive Nachteile sein können. Es ist mitunter kein 
gangbarer Weg, die CI-Pipeline stundenlang mit rebuilds zu verstopfen, 
die gar nicht nötig sind. Wenn z.B. ein wichtiger Kunde bei uns sagt, 
dass er eine neue Version mit aktuellen Änderungen benötigt, soll die 
innerhalb von 30 Minuten zur Verfügung stehen. Und das für 4 
Plattformen. Außerdem werden Patches mit so einem monolithischem 
Approach sehr viel umfangreicher.
Ist natürlich nicht so die uC-Welt, aber wir mussten echt schon 
tricksen, um das hinzubekommen...
Die Flexibilität würde ich anzweifeln, da man mit einem Monolithen nicht 
so einfach Teile zur Laufzeit austauschen kann (lade dieses .so statt 
jenem).

: Bearbeitet durch User
von DPA (Gast)


Lesenswert?

Also in c gibt deshalb das restrict keyword: 
https://en.wikipedia.org/wiki/Restrict

von Jemand (Gast)


Lesenswert?

Wilhelm M. schrieb:
> Aliasing oder warum es (fast immer) falsch ist, mehrstellige freie
> Funktionen mit primitiven Datentypen zu verwenden oder der Teufel steckt
> auch im char.

Dann sind Sprachen ohne strict aliasing ja überhaupt nicht richtig 
benutzbar. Tipp: in der Praxis sind die nicht langsamer.
Das ist zu 99% völlige Zeitverschwendung, solange der Profiler nichts 
anderes behauptet.

von Heiko L. (zer0)


Lesenswert?

DPA schrieb:
> Also in c gibt deshalb das restrict keyword:
> https://en.wikipedia.org/wiki/Restrict

Jemand schrieb:
> Dann sind Sprachen ohne strict aliasing ja überhaupt nicht richtig
> benutzbar.

Primitive Datentypen übergibt man doch sowieso by Value. Aliasing 
ausgechlossen.

: Bearbeitet durch User
von mh (Gast)


Lesenswert?

Wilhelm M. schrieb:
> Dies bringt noch weitere Vorteile wie inlining, header-only ohne
> Architekturcompilate, Flexibilität, ... mit sich,
> was sich zusätzlich positiv auf die Performance auswirken kann.

Da liegt das Problem für mich. Du sprichst nur von Laufzeitperformance. 
Compiletimeperformance ist dir egal. Ich kann keine 5 Minuten 
verschwenden, wenn ich nur 1 Stunde Rechenzeit habe.

Mein aktuelles Projekt als Beispiel besteht zu 90% aus Fortran und 10% 
C++ (Zeilen nach Präpro/alle includes aufgelöst). Beim Compilieren macht 
C++ allerdings mehr als 90% der Zeit aus. Und ich bin schon sehr sparsam 
in Sachen Templates.

von Wilhelm M. (wimalopaan)


Lesenswert?

Heiko L. schrieb:
> Wilhelm M. schrieb:
>> iii) Auch für alle anderen primitiven Datentypen sog. StrongType's
>> verwenden (ggf. als template).
>
> Das nützt dir in Bezug auf Aliasing wenig, solange die Typen
> pointer-interconvertible sind.

Doch, genau da nützt es was: siehe Code mit POD S oder T.
Ein Aliasing findet immer mit Typaliasen von char oder std::byte statt 
(vorgeschrieben, Grund steht auch oben). Siehe angehängten Code.

Heiko L. schrieb:
> Edit: Und auch dann wäre ich mir nicht sicher, dass nicht final gilt
> "ein int ist ein int".

Nope. Funktioniert alles wie gewünscht.

von Wilhelm M. (wimalopaan)


Lesenswert?

Heiko L. schrieb:
> Wobei das auch massive Nachteile sein können. Es ist mitunter kein
> gangbarer Weg, die CI-Pipeline stundenlang mit rebuilds zu verstopfen,
> die gar nicht nötig sind. Wenn z.B. ein wichtiger Kunde bei uns sagt,
> dass er eine neue Version mit aktuellen Änderungen benötigt, soll die
> innerhalb von 30 Minuten zur Verfügung stehen. Und das für 4
> Plattformen. Außerdem werden Patches mit so einem monolithischem
> Approach sehr viel umfangreicher.

Wenn das bei Euch so ist, dann ist es eben so. Es gibt aber auch viele 
Fälle, wo man daraus große Vorteile ziehen kann.

Heiko L. schrieb:
> Die Flexibilität würde ich anzweifeln, da man mit einem Monolithen nicht
> so einfach Teile zur Laufzeit austauschen kann (lade dieses .so statt
> jenem).

Das hat mit header-only ja nichts zu tun.

von Wilhelm M. (wimalopaan)


Lesenswert?

Heiko L. schrieb:
> Primitive Datentypen übergibt man doch sowieso by Value. Aliasing
> ausgechlossen.

Ja, genau.
Es ging hier aber um Output-Parameter. Also R/W-Referenzen in Form von 
non-const c++-references oder pointer. Und hier steckt eben der Teufel 
im char*.

von Wilhelm M. (wimalopaan)


Lesenswert?

mh schrieb:
> Wilhelm M. schrieb:
>> Dies bringt noch weitere Vorteile wie inlining, header-only ohne
>> Architekturcompilate, Flexibilität, ... mit sich,
>> was sich zusätzlich positiv auf die Performance auswirken kann.
>
> Da liegt das Problem für mich. Du sprichst nur von Laufzeitperformance.
> Compiletimeperformance ist dir egal.

Nein, aber nicht entscheidend.

von Wilhelm M. (wimalopaan)


Lesenswert?

mh schrieb:
> Mein aktuelles Projekt als Beispiel besteht zu 90% aus Fortran und 10%
> C++ (Zeilen nach Präpro/alle includes aufgelöst). Beim Compilieren macht
> C++ allerdings mehr als 90% der Zeit aus.

Dann sollte man da mal genauer schauen. Würde ich ja wohl lohnen.

> Und ich bin schon sehr sparsam
> in Sachen Templates.

Du vielleicht. Vielleicht ist es aber einfach eine grotten schlecht 
realisierte template-Bibliothek, die Du da verwendest. Denn man kann mit 
TMP Code schreiben, der zu Compilezeit langsam ist oder auch schnell.

von mh (Gast)


Lesenswert?

Wilhelm M. schrieb:
> mh schrieb:
>> Mein aktuelles Projekt als Beispiel besteht zu 90% aus Fortran und 10%
>> C++ (Zeilen nach Präpro/alle includes aufgelöst). Beim Compilieren macht
>> C++ allerdings mehr als 90% der Zeit aus.
>
> Dann sollte man da mal genauer schauen. Würde ich ja wohl lohnen.
>
>> Und ich bin schon sehr sparsam
>> in Sachen Templates.
>
> Du vielleicht. Vielleicht ist es aber einfach eine grotten schlecht
> realisierte template-Bibliothek, die Du da verwendest. Denn man kann mit
> TMP Code schreiben, der zu Compilezeit langsam ist oder auch schnell.

Ich benutze keine template-Bibliothek. Alle Bibliotheken, die ich bis 
jetzt getestet habe, waren schrecklich zu benutzen und haben 
Compilezeiten extrem verlängert. Sie sind auch overkill für die 
Templates, die ich brauche.

Wilhelm M. schrieb:
> Heiko L. schrieb:
>> Primitive Datentypen übergibt man doch sowieso by Value. Aliasing
>> ausgechlossen.
>
> Ja, genau.
> Es ging hier aber um Output-Parameter. Also R/W-Referenzen in Form von
> non-const c++-references oder pointer. Und hier steckt eben der Teufel
> im char*.
Mehrstellige Funktionen sind böse, aber Output-Parameter sind ok? Und 
fehlende Optimierungen aufgrund von aliasing ist das kleinste Problem 
mit char*.

von Wilhelm M. (wimalopaan)


Lesenswert?

mh schrieb:
>>> Und ich bin schon sehr sparsam
>>> in Sachen Templates.
>>
>> Du vielleicht. Vielleicht ist es aber einfach eine grotten schlecht
>> realisierte template-Bibliothek, die Du da verwendest. Denn man kann mit
>> TMP Code schreiben, der zu Compilezeit langsam ist oder auch schnell.
>
> Ich benutze keine template-Bibliothek.

Ja, was denn nun?

> Alle Bibliotheken, die ich bis
> jetzt getestet habe, waren schrecklich zu benutzen und haben
> Compilezeiten extrem verlängert.

Verwendest Du nicht die C++-Standardbibliothek?

> Sie sind auch overkill für die
> Templates, die ich brauche.

Also, jetzt doch wieder ...?

von mh (Gast)


Lesenswert?

Wilhelm M. schrieb:
> mh schrieb:
>>>> Und ich bin schon sehr sparsam
>>>> in Sachen Templates.
>>>
>>> Du vielleicht. Vielleicht ist es aber einfach eine grotten schlecht
>>> realisierte template-Bibliothek, die Du da verwendest. Denn man kann mit
>>> TMP Code schreiben, der zu Compilezeit langsam ist oder auch schnell.
>>
>> Ich benutze keine template-Bibliothek.
>
> Ja, was denn nun?
Was genau meinst du mit template-Bibliothek? Ich verstehe darunter sowas 
wie Hana.

Wilhelm M. schrieb:
> mh schrieb:
>> Alle Bibliotheken, die ich bis
>> jetzt getestet habe, waren schrecklich zu benutzen und haben
>> Compilezeiten extrem verlängert.
>
> Verwendest Du nicht die C++-Standardbibliothek?
>
Ich verwende die C++-Standardbibliothek sparsam. Das was ich benutze ist 
im wesentlichen std::vector, die C-Standardbibliothek und selten 
algorithm. Aber du willst doch sicher nicht andeuten, dass die 
Standardbibliothek, die die Compiler mitlieferen, grotten schlecht sind?

Wilhelm M. schrieb:
> mh schrieb:
>> Sie sind auch overkill für die
>> Templates, die ich brauche.
>
> Also, jetzt doch wieder ...?
Wo ist dein Problem?

von Wilhelm M. (wimalopaan)


Lesenswert?

mh schrieb:
> Wilhelm M. schrieb:
>> mh schrieb:
>>>>> Und ich bin schon sehr sparsam
>>>>> in Sachen Templates.
>>>>
>>>> Du vielleicht. Vielleicht ist es aber einfach eine grotten schlecht
>>>> realisierte template-Bibliothek, die Du da verwendest. Denn man kann mit
>>>> TMP Code schreiben, der zu Compilezeit langsam ist oder auch schnell.
>>>
>>> Ich benutze keine template-Bibliothek.
>>
>> Ja, was denn nun?
> Was genau meinst du mit template-Bibliothek? Ich verstehe darunter sowas
> wie Hana.

Eine template-Bibliothek ist eine C++-Bibliothek, die templates zur 
Verfügung stellt. Nicht mehr oder weniger.

Boost-Hana ist auch eine template-bibliothek, die sich aber mit 
Template-Meta-Programmierung in einem ganz besonderen Stil (nicht der 
klassische Meta-Funktions-Ansatz) beschäftigt.

Da haben wir uns ggf. etwas missverstanden.

mh schrieb:
> Ich verwende die C++-Standardbibliothek sparsam. Das was ich benutze ist
> im wesentlichen std::vector, die C-Standardbibliothek und selten
> algorithm.

Gerade <algorithm> solltest Du häufiger verwenden ;-)

mh schrieb:
> Aber du willst doch sicher nicht andeuten, dass die
> Standardbibliothek, die die Compiler mitlieferen, grotten schlecht sind?

Zumindest kann ich für den GCC/g++ sagen, dass die Implementierung gut. 
Wobei man natürlich auch hier viele Dinge besser (im Sinne von 
Compilezeitverbrauch) machen könnte, wenn man etwa nur > C++17 
unterstützen würde.

mh schrieb:
>>
>> Also, jetzt doch wieder ...?
> Wo ist dein Problem?

Einmal sagst, Du verwendest templates sparsam, dann sagst Du, Du 
verwendest keine templates, nun nun wieder doch ... aber ok, Du meintest 
wohl TMP.

von mh (Gast)


Lesenswert?

Wilhelm M. schrieb:
> Da haben wir uns ggf. etwas missverstanden.
Sieht so aus. Allerdings gibt es dann keine nicht-template-Bibliothek, 
da im jede C++-Bibliothek, die mir über den Weg gelaufen ist mindestens 
ein Template zur Verfügung stellt.

Wilhelm M. schrieb:
> Gerade <algorithm> solltest Du häufiger verwenden ;-)
Nein sollte ich nicht. Ich weiß was drin steht und vieles davon ist auf 
den ersten Blick praktisch. Aber in der Anwendung fehlt dann meistens 
ein
1
if cond() {
2
  break;
3
}
an der richtigen Stelle, um frühzeitig abzubrechen. Oder ich muss 3 
Funktionen und ein Lambda schachteln, um das gleiche wie eine 
übersichtliche 3 zeilige for-Schleife zu erreichen.

Wilhelm M. schrieb:
> Einmal sagst, Du verwendest templates sparsam, dann sagst Du, Du
> verwendest keine templates, nun nun wieder doch ... aber ok, Du meintest
> wohl TMP.
Das ist wohl eine Folge des obigen Missverständnisses. Ich benutze 
templates sparsam. Eine Funktion, die beliebige callables als als 
Callback akzeptiert, ist ok. Eine Funktion die 10 Zeilen SFINAE und ein 
Lambda benötigt, um eine 3 Zeilen for-Schleife zu ersetzen, ist nicht 
ok. Ein paar strong-types einführen, um ne uralte c-api brauchbar zu 
machen ist ok. Ein UDL für Einheiten und Präfixe ist nicht ok, da es bis 
jetzt keinen Fehler verhindert hätte. Wenn ich auf die Idee komme CRTP 
einzusetzen um ein Problem zu lösen, ist das für mich ein Zeichen dafür, 
über Alternativen nachzudenken. Das gleiche gilt für komplexes SFINAE. 
Eine einfaches is_convertible, um einen Constructor zu verstecken ist 
ok. 5 Zeilen SFINAE, um drei überladene nicht template Funktionen zu 
einem Template zusammenzufügen, ist nicht ok.

Genauso vermeide ich Bibliotheken die es übertreiben. Ein Beispiel dafür 
ist xtensor. Das was die Bibliothek verspricht, ist genau das was ich 
brauche. Allerdings funktioniert nichts wie man es erwartet, die 
Fehlermeldungen sind nichtsagende 500 Zeilen Monster und damit 
unbrauchbar und die Compilezeiten sind absurd.

von Wilhelm M. (wimalopaan)


Lesenswert?

mh schrieb:
> Aber in der Anwendung fehlt dann meistens
> einif cond() {
>   break;
> }

Beispiel?

mh schrieb:
> Oder ich muss 3
> Funktionen und ein Lambda schachteln, um das gleiche wie eine
> übersichtliche 3 zeilige for-Schleife zu erreichen.

Beispiel?

mh schrieb:
> Eine Funktion die 10 Zeilen SFINAE und ein
> Lambda benötigt, um eine 3 Zeilen for-Schleife zu ersetzen, ist nicht
> ok.

Beispiel?

mh schrieb:
> 5 Zeilen SFINAE, um drei überladene nicht template Funktionen zu
> einem Template zusammenzufügen, ist nicht ok.

Beispiel?

von Heiko L. (zer0)


Lesenswert?

Wilhelm M. schrieb:
> Doch, genau da nützt es was: siehe Code mit POD S oder T.
> Ein Aliasing findet immer mit Typaliasen von char oder std::byte statt
> (vorgeschrieben, Grund steht auch oben). Siehe angehängten Code.

Wenn deine class mit z.B. int* pointer-interconvertible ist, kann jede 
int-Referenz die struct (bzw. den int darin) aliasen, oder nicht?
Also zB
1
void (struct X& x, int*p) {
2
  cout<<x;
3
  *p = 2;
4
  cout<<x; // 2?
5
}

von Wilhelm M. (wimalopaan)


Lesenswert?

Heiko L. schrieb:
> Wenn deine class mit z.B. int* pointer-interconvertible ist, kann jede
> int-Referenz die struct (bzw. den int darin) aliasen, oder nicht?

Du meinst in meinem Beispiel:
1
uint8_t testM() {
2
    S   x1;
3
    int x2;
4
    return test1(x1, x2);
5
}

Nein, natürlich nicht. Sind unterschiedliche Typen:
1
auto test1<S, int>(S&, int&):
2
        mov     BYTE PTR [rdi], 1
3
        mov     eax, 3
4
        mov     DWORD PTR [rsi], 2
5
        ret

Kein reread.

von Heiko L. (zer0)


Lesenswert?

Wilhelm M. schrieb:
> Heiko L. schrieb:
>> Wenn deine class mit z.B. int* pointer-interconvertible ist, kann jede
>> int-Referenz die struct (bzw. den int darin) aliasen, oder nicht?
>
> Du meinst in meinem Beispiel:
>
> uint8_t testM() {
>     S   x1;
>     int x2;
>     return test1(x1, x2);
> }
>
> Nein, natürlich nicht. Sind unterschiedliche Typen:
> auto test1<S, int>(S&, int&):
>         mov     BYTE PTR [rdi], 1
>         mov     eax, 3
>         mov     DWORD PTR [rsi], 2
>         ret
>
> Kein reread.

Da ist was durcheinander. Schau hier:
https://godbolt.org/z/GzvirT

Edit: Dummer weise scheint sich der Effekt auch nicht durch hinzufügen 
des Tags als member beheben zu lassen. Ich bin nicht sicher, ob das eine 
ausgelassene Optimierung ist oder ob das aliasing trotzdem noch möglich 
wäre: Eigentlich gibt es keinen Weg, da an die Addresse des int zu 
kommen? Oder doch?

: Bearbeitet durch User
von Wilhelm M. (wimalopaan)


Lesenswert?

Das ist ja ein Spezialfall: pointer-interconvertible kommt zum aliasing 
hinzu, d.h. in diesem Fall muss von einem aliasing ausgegangen werden.

Aber:
1
template<typename T>
2
struct X {int k; };
3
4
int fn(X<struct A>& x, X<struct B>& p) {
5
    int i = x.k;
6
    p.k = 2;
7
    return i + x.k;
8
}

Kein Reread
1
fn(X<A>&, X<B>&):
2
        mov     eax, DWORD PTR [rdi]
3
        mov     DWORD PTR [rsi], 2
4
        add     eax, eax
5
        ret

Also: strong types helfen.

In meinem ursprünglichen Beispiel war das nicht gegeben. Deswegen auch 
dort kein reread.

: Bearbeitet durch User
von Heiko L. (zer0)


Lesenswert?

Wilhelm M. schrieb:
> Das ist ja ein Spezialfall: pointer-interconvertible kommt zum aliasing
> hinzu, d.h. in diesem Fall muss von einem aliasing ausgegangen werden.

Ja, das war der "einfache" Fall. Und hier?

https://godbolt.org/z/B35U2u

von Sven B. (scummos)


Lesenswert?

Jemand schrieb:
> Das ist zu 99% völlige Zeitverschwendung, solange der Profiler nichts
> anderes behauptet.

Das sehe ich ganz genau so. Das Performance-Problem fast jeder modernen 
C++-Anwendung liegt vordergründig darin, dass irgendjemand mit zu viel 
C++-Template-Wissen einen wirren Wald aus "effizientem" Kram gebaut hat, 
den die 34 anderen Entwickler nicht verstehen, falsch anwenden, und 
damit unlesbaren und langsamen Code erzeugen.

Trotzdem interessanter Post, der Inhalt war mir nicht bekannt.

von Wilhelm M. (wimalopaan)


Lesenswert?

Heiko L. schrieb:
> Und hier?

Interessant. Sollte ja

- nicht pointer-interconvertible sein
- und kein direktes aliasing.

Es wird aber aliased-code generiert. Habe ich sofort keine Antwort 
darauf.

von Heiko L. (zer0)


Lesenswert?

Wilhelm M. schrieb:
> Heiko L. schrieb:
>> Und hier?
>
> Interessant. Sollte ja
>
> - nicht pointer-interconvertible sein
> - und kein direktes aliasing.
>
> Es wird aber aliased-code generiert. Habe ich sofort keine Antwort
> darauf.

Ja, da bin ich echt am grübeln. Über einen char* iterieren und 
"zufällig" an der richtigen Stelle auf int* casten?
Einerseits: Wo ein int ist, ist ein int.
Andererseits: Man kann nicht gewusst haben, dass da einer steht. Also 
UB.

von Yalu X. (yalu) (Moderator)


Lesenswert?

Ich glaube, fast jeder Softwareentwickler wird folgende beide Regeln
akzeptieren:

Regel 1:
Argumenttypen einer Funktion sollten so gewählt werden, dass sie logisch
sinnvoll und für den Anwender der Funktion nachvollziehbar sind.

Regel 2:
Der Anwender einer Funktion sollte nicht unnötig mit irgendwelchen
Interna der Funktion konfrontiert werden.

Die hier diskutierte Verwendung von gekünstelten Argumenttypen mit dem
alleinigen Ziel, innerhalb der Funktion eine leichte Optimierung zu
ermöglichen, verstößt gleich gegen alle beide Regeln.

Das Nachladen von Objekten innerhalb der Funktion kann auch ohne
Beeinflussung der Funktionsschnittstelle vermieden werden, bspw.

- durch Änderung der Zugriffsreihenfolge (geeignetes Umstellen der
  Anweisungen innerhalb der Funktion)

- wenn das nicht zum Ziel führt, durch Zwischenspeicherung der von
  Aliasing betroffenen Objekte in lokalen Variablen

- in C auch durch die Verwendung von restrict

Bevor man aber mit solchen Detailoptimierungen anfängt, sollte man sich
überlegen, ob damit überhaupt ein merklicher Geschwindigkeitsvorteil
erzielt werden kann. Meist ist das aus den folgenden Gründen nämlich
nicht der Fall:

Bei größeren Funktionen fällt ein zusätzlicher Speicherzugriff kaum ins
Gewicht, zumal das Objekt beim zweiten Zugriff ja schon im Cache liegt.

Bei zeitkritischen kleinen Funktionen wird man i.Allg. dafür sorgen,
dass sie geinlinet werden. Dadurch kann der Compiler meist selbst
erkennen, ob ein Aliasing stattfindet oder nicht, und den Code
entsprechend selber optimieren.

Für die ganz wenigen verbleibenden Fälle, wo eine händische Optimierung
sinnvoll ist, kann man sich der oben aufgezählten Mittel bedienen. Auf
keinen Fall würde ich aber deswegen die Funktionsschnittstelle ändern.

von Wilhelm M. (wimalopaan)


Lesenswert?

Wilhelm M. schrieb:
> Es wird aber aliased-code generiert

Naj, die Antwort ist eigentlich ganz einfach: p kann einfach das 
int-member von X referenzieren: deswegen muss der Compiler aliased code 
generieren. Sorry, hätte ich sofort sehen müssen.

von Wilhelm M. (wimalopaan)


Lesenswert?

Yalu X. schrieb:
> Die hier diskutierte Verwendung von gekünstelten Argumenttypen mit dem
> alleinigen Ziel, innerhalb der Funktion eine leichte Optimierung zu
> ermöglichen, verstößt gleich gegen alle beide Regeln.

Andersherum: die korrekte Verwendung von passenden Datentypen führt 
zwangsläufig zu besserem Code. Der Anwender muss über gar nicht 
nachdenken.

von Heiko L. (zer0)


Lesenswert?

Wilhelm M. schrieb:
> Naj, die Antwort ist eigentlich ganz einfach: p kann einfach das
> int-member von X referenzieren: deswegen muss der Compiler aliased code
> generieren. Sorry, hätte ich sofort sehen müssen.

Woher soll denn die Referenz kommen? Der member ist private und die 
Addresse wird nicht herausgegeben. Außerdem ist es kein Standard-Layout.
Ich sehe keinen "unanfechtbar gültigen" Weg, an die Addresse zu kommen.

: Bearbeitet durch User
von Wilhelm M. (wimalopaan)


Lesenswert?

Heiko L. schrieb:
> Woher soll denn die Referenz kommen?

Nun, ich denke, der Compiler geht hier einfach pessimistisch vor. Er ist 
ja nicht gezwungen, die Optimierung vorzunehmen. Solange er nicht wegen 
Typungleichheit (abgesehen von den immer alias-type char* und std::byte* 
und ihren typ-aliasen) absolut ein Aliasing ausschließend kann, bleibt 
es konservativ.

von Yalu X. (yalu) (Moderator)


Lesenswert?

Heiko L. schrieb:
> Woher soll denn die Referenz kommen? Der member ist private und die
> Addresse wird nicht herausgegeben.

In diesem einfachen Fall, wo die komplette Implementierung der Klasse
für den Compiler sichtbar ist, könnte er theoretisch erkennen, dass hier
kein Aliasing stattfinden kann. Aber stell dir vor, X enthielte 1000
weitere Memberfunktionen, von denen eine einzige evtl. die Adresse von k
veröffentlicht. Der Compiler müsste dann alle 100 Memberfunktionen
danach durchsuchen, was ziemlich zeitaufwendig sein kann. Dazu kommt:
Wenn mindestens eine dieser Memberfunktionen in einer anderen
Übersetzungseinheit definiert ist, hat der Compiler überhaupt keine
Möglichkeit mehr, ein Aliasing auszuschließen.

Deswegen nehme an, dass der Compiler der Einfachheit halber nach der
Regel arbeitet: Jedes int-Objekt – egal wie verborgen – kann prinzipiell
über jeden int-LValue potentiell geändert werden.

: Bearbeitet durch Moderator
von Heiko L. (zer0)


Lesenswert?

Yalu X. schrieb:
> Heiko L. schrieb:
>> Woher soll denn die Referenz kommen? Der member ist private und die
>> Addresse wird nicht herausgegeben.
>
> In diesem einfachen Fall, wo die komplette Implementierung der Klasse
> für den Compiler sichtbar ist, könnte er theoretisch erkennen, dass hier
> kein Aliasing stattfinden kann. Aber stell dir vor, X enthielte 1000
> weitere Memberfunktionen, von denen eine einzige evtl. die Adresse von k
> veröffentlicht. Der Compiler müsste dann alle 100 Memberfunktionen
> danach durchsuchen, was ziemlich zeitaufwendig sein kann.
>
> Deswegen nehme an, dass der Compiler der Einfachheit halber nach der
> Regel arbeitet: Jedes int-Objekt – egal wie verborgen – kann prinzipiell
> über jeden int-LValue potentiell geändert werden.

Naja, solche Typen, wie sie Wilhelm vorschweben, sind in der Regel nicht 
so umfangreich und offenliegend. Ich hätte gedacht, dass die Compiler da 
solche Analysen machen.

von Wilhelm M. (wimalopaan)


Lesenswert?

Da sieht man wieder den Vorteil von templates ...

Beitrag "Re: (No)-Aliasing for fun and performance"

von Yalu X. (yalu) (Moderator)


Lesenswert?

Wilhelm M. schrieb:
> Yalu X. schrieb:
>> Die hier diskutierte Verwendung von gekünstelten Argumenttypen mit dem
>> alleinigen Ziel, innerhalb der Funktion eine leichte Optimierung zu
>> ermöglichen, verstößt gleich gegen alle beide Regeln.
>
> Andersherum: die korrekte Verwendung von passenden Datentypen führt
> zwangsläufig zu besserem Code. Der Anwender muss über gar nicht
> nachdenken.

Für mich kann der passendste Datentyp auch ein int oder ein char sein.

Aber das Thema mit den neudefinierten Datentypen für sämtliche
Funktionsargumente hatten wir ja schon im anderen Thread:

  Beitrag "Typsichere und permutierte Parameterlisten in C++"

Meine Meinung darüber ist nach wie vor diese:

  Beitrag "Re: Typsichere und permutierte Parameterlisten in C++"

Das Wrappen in so genannte Strong Types ist ja an sich keine schlechte
Idee, aber in C++ wegen mangelnder Unterstützung durch die Sprache nur
sehr holprig umsetzbar. So zeigt bspw. Haskell mit newtype und
Funktionsparameter-Patterns, wie so etwas richtig geht. Dort ist
deswegen auch meine Abneigung gegenüber Wrapper-Typen nicht so groß :)

von Wilhelm M. (wimalopaan)


Lesenswert?

Sven B. schrieb:
> Das sehe ich ganz genau so. Das Performance-Problem fast jeder modernen
> C++-Anwendung liegt vordergründig darin, dass irgendjemand mit zu viel
> C++-Template-Wissen einen wirren Wald aus "effizientem" Kram gebaut hat,
> den die 34 anderen Entwickler nicht verstehen, falsch anwenden, und
> damit unlesbaren und langsamen Code erzeugen.

Wenn jemand etwas falsch macht, dann wird's wohl falsch sein ;-)

Und das ist der Grund, warum std::sort() immer mindestens ca. 2-4-mal 
schneller ist als qsort()?

von Wilhelm M. (wimalopaan)


Lesenswert?

Yalu X. schrieb:
> Für mich kann der passendste Datentyp auch ein int oder ein char sein.

Selbstverständlich, dagegen ist auch nichts einzuwenden, sofern es 
"richtig" gemacht wird, also etwa bei einstelligen Funktion.

Yalu X. schrieb:
> Aber das Thema mit den neudefinierten Datentypen für sämtliche
> Funktionsargumente hatten wir ja schon im anderen Thread:

Es ja nicht um sämtliche Parameter einer Funktion. Es geht darum, eine 
Schnittstelle sicher zu gestalten, und um den Typen eine Semantik zu 
geben. Ein `int` oder `char*` trägt keine Semantik.

von S. R. (svenska)


Lesenswert?

Wilhelm M. schrieb:
> Ein `int` oder `char*` trägt keine Semantik.

Doch: "Hier ist eine Zahl." oder "Hier ist ein Zeichen."

Die Semantik ist jetzt vielleicht ganz so ausführlich wie "Hier steht 
ein Zeichen eines Error-Strings in Modul X, welches für Anwendung Y in 
Funktion Z benutzt wird."

Eins der großen Projekte auf Arbeit abstrahiert alle Datentypen sauber 
weg. Man rennt also bei der Codeanalyse grundsätzlich erstmal mehreren 
Abstraktionsebenen hinterher, nur um festzustellen, welcher Wert jetzt 
eigentlich zwischen den APIs übergeben und umkonvertiert wird.

Und da grundsätzlich alle Datentypen sauber abstrahiert werden, gibt 
es auch mehrere Implementationen relativ primitiver Typen, z.B. Size 
oder Rectangle. Schließlich kann sich die Rechteck-Definition in einem 
Modul ja auch von der Definition in einem anderen Modul unterscheiden, 
sofern sie nicht miteinander kommunizieren. Einheiten sind auch ein 
wunderbarer Quell der Freude, sowohl in Format als auch Verwendung.

Ja, ein ausführliches Typsystem ist ein stetiger Quell der Freude.

: Bearbeitet durch User
von Sven B. (scummos)


Lesenswert?

Wilhelm M. schrieb:
> Sven B. schrieb:
>> Das sehe ich ganz genau so. Das Performance-Problem fast jeder modernen
>> C++-Anwendung liegt vordergründig darin, dass irgendjemand mit zu viel
>> C++-Template-Wissen einen wirren Wald aus "effizientem" Kram gebaut hat,
>> den die 34 anderen Entwickler nicht verstehen, falsch anwenden, und
>> damit unlesbaren und langsamen Code erzeugen.
>
> Wenn jemand etwas falsch macht, dann wird's wohl falsch sein ;-)

Es gibt aber Dinge, die sind leichter falsch zu machen als andere ;)

> Und das ist der Grund, warum std::sort() immer mindestens ca. 2-4-mal
> schneller ist als qsort()?

Ich bestreite nicht den generellen Wert von Metaprogrammierung zum 
Erstellen generischer und effizienter APIs. Ich stelle Aussagen wie

> iii) Auch für alle anderen primitiven Datentypen sog. StrongType's
verwenden (ggf. als template).

als allgemeinen Tipp in Frage. In meiner Erfahrung wird Code erst einmal 
schnell dadurch, dass man ihn übersichtlich aufschreibt, dann profiled 
und dann optimiert, was eigentlich langsam ist, meist z.B. durch bessere 
Algorithmen, geschicktes Caching, oder bessere Datenstrukturen. Erst 
wenn hier aller Spielraum ausgeschöpft ist und noch mehr Performance 
benötigt wird, kann man über Spielereien mit der Sprache nachdenken. Das 
bringt nämlich idR viel weniger und erschwert zudem den anderen Ansatz 
stark, weil es die Komplexität und Anzahl der Zeilen erhöht.

von Yalu X. (yalu) (Moderator)


Lesenswert?

Wilhelm M. schrieb:
> Yalu X. schrieb:
>> Aber das Thema mit den neudefinierten Datentypen für sämtliche
>> Funktionsargumente hatten wir ja schon im anderen Thread:
>
> Es ja nicht um sämtliche Parameter einer Funktion. Es geht darum, eine
> Schnittstelle sicher zu gestalten, und um den Typen eine Semantik zu
> geben. Ein `int` oder `char*` trägt keine Semantik.

Ja, aber diese Schnittstelle muss deiner Ansicht nach so gestaltet sein,
dass keine zwei Argumente denselben Typ haben.

Vom potentiellen Aliasing sind übrigens nicht nur Zeiger und Referenzen
in Funktionsargumenten, sondern auch in Member- und globalen Variablen,
auf die in der Funktion zugegriffen wird, betroffen. Konsequenterweise
müsstest du die Typen so definieren, dass (mindestens) folgendes erfüllt
ist:

- Keine Member- und keine globale Variable darf ein char referenzieren.

- Keine zwei Membervariablen, globale Variablen oder Funktionsargumente
  dürfen denselben Typ referenzieren.

- Keine Membervariable, globale Variable oder Funktionsargument darf
  einen Typ referenzieren, der bereits für eine andere Member- oder
  globale Variable verwendet wird.

Um sicher zu gehen, dass nirgends ein unerwünschtes Nachladen von
Objekten stattfindet, müsstest du praktisch alle von Membervariablen,
globalen Variablen und Funktionsargumenten referenzierten Objekte, für
die die obigen Bedingungen nicht erfüllt sind, in Strong Types
verpacken, und zwar so, dass diese Strong Types im selben Scope nicht
ein zweites Mal auftauchen.

Kannst du mir eine nicht ganz triviale Open-Source-Software nennen, wo
das jemand (du oder ein anderer) einmal komplett durchgezogen hat?

Wenn nicht, dann geh einfach davon aus, dass deine Vorstellungen – so
gut gemeint sie auch sein mögen – nicht sinnvoll umsetzbar sind. Ich bin
mir nämlich fast sicher, dass ein solcher Code kaum lesbar wäre.

Und wenn dann doch einmal eine Variable wegen des potentiellen Aliasing
nachgeladen wird und damit ein paar Nanosekunden Rechenzeit mehr
verbraten werden, wird die Welt deswegen auch nicht untergehen :)

von Heiko L. (zer0)


Lesenswert?

Wilhelm M. schrieb:
> Da sieht man wieder den Vorteil von templates ...

Ich persönlich ziehe StrongTypes nur da in Betracht, wo sie die richtige 
Bedinung vereinfachen, also den Komfort erhöhen, indem sie Tipparbeit 
sparen  oder ähnliches. Das schließt ihre Benutzung zur Vermeidung von 
Fehlern beinahe aus. Bestimmt aber, benannte Parameter mit Typen zu 
emulieren.

: Bearbeitet durch User
von Rolf M. (rmagnus)


Lesenswert?

Wilhelm M. schrieb:
> Dies führt fast unmittelbar dazu, dass man (fast) allen Code als
> template schreibt.
>
> Dies bringt noch weitere Vorteile wie inlining, header-only ohne
> Architekturcompilate, Flexibilität, ... mit sich,
> was sich zusätzlich positiv auf die Performance auswirken kann.

Wieso wird inlining und header-only so gerne als großer Vorteil von 
Templates angepriesen? Das kann man ohne Templates doch ganz genauso 
haben.
Da heißt es z.B. öfter mal so was wie "Boost:xyz ist ganz toll, weil es 
Templates benutzt und deshalb header-only ist". Wenn das so toll ist, 
warum sind dann nicht alle Biblotheken header-only?
Bei Templates ist es lediglich so, dass es nicht anders geht, was bei 
großen Projekten dann zu elends langen Compilezeiten und 
unübersichtlichen Fehlermeldungen führen kann. Es ist also ein Nachteil, 
kein Vorteil von Templates.

Wilhelm M. schrieb:
> Heiko L. schrieb:
>> Die Flexibilität würde ich anzweifeln, da man mit einem Monolithen nicht
>> so einfach Teile zur Laufzeit austauschen kann (lade dieses .so statt
>> jenem).
>
> Das hat mit header-only ja nichts zu tun.

Doch, klar. Wie soll ich eine Header-Only-Bibliothek als shared library 
einbinden? Sie wird ja fest mit ins Programm einkompiliert.

Wilhelm M. schrieb:
>> Da liegt das Problem für mich. Du sprichst nur von Laufzeitperformance.
>> Compiletimeperformance ist dir egal.
>
> Nein, aber nicht entscheidend.

Für mich ist sie das schon. Wenn ich nach jeder kleinen Änderung 10 
Minuten auf den Compiler warten muss, bevor ich ausprobieren kann, ob 
sie tut, weil er jedes mal Massen an Templates neu übersetzen muss, dann 
macht das meine Arbeit ineffizient.

mh schrieb:
> Wilhelm M. schrieb:
>> Da haben wir uns ggf. etwas missverstanden.
> Sieht so aus. Allerdings gibt es dann keine nicht-template-Bibliothek,
> da im jede C++-Bibliothek, die mir über den Weg gelaufen ist mindestens
> ein Template zur Verfügung stellt.

Also für mich ist eine Template-Bibliothek eine, die vollständig oder 
zumindest maßgeblich aus Templates besteht. Qt ist z.B. keine, auch wenn 
sie durchaus einige Template-Klassen zur Verfügung stellt.

Wilhelm M. schrieb:
> Yalu X. schrieb:
>> Für mich kann der passendste Datentyp auch ein int oder ein char sein.
>
> Selbstverständlich, dagegen ist auch nichts einzuwenden, sofern es
> "richtig" gemacht wird, also etwa bei einstelligen Funktion.

Es ist doch absurd, den Parameter-Typ davon abhängig zu machen, wie 
viele weitere Parameter die Funktion hat.

von Wilhelm M. (wimalopaan)


Lesenswert?

Rolf M. schrieb:
> Wilhelm M. schrieb:
>> Heiko L. schrieb:
>>> Die Flexibilität würde ich anzweifeln, da man mit einem Monolithen nicht
>>> so einfach Teile zur Laufzeit austauschen kann (lade dieses .so statt
>>> jenem).
>>
>> Das hat mit header-only ja nichts zu tun.
>
> Doch, klar. Wie soll ich eine Header-Only-Bibliothek als shared library
> einbinden? Sie wird ja fest mit ins Programm einkompiliert.

Eine header-only-C++-Bibliothek und ein dynamic-link-object sind nunmal 
zwei vollkommen unterschiedliche Sachen. Aber Du kannst aus einer 
Header-Only-Bibliothek auch ein dynamic-link-object erstellen.

Rolf M. schrieb:
> Wilhelm M. schrieb:
>> Yalu X. schrieb:
>>> Für mich kann der passendste Datentyp auch ein int oder ein char sein.
>>
>> Selbstverständlich, dagegen ist auch nichts einzuwenden, sofern es
>> "richtig" gemacht wird, also etwa bei einstelligen Funktion.
>
> Es ist doch absurd, den Parameter-Typ davon abhängig zu machen, wie
> viele weitere Parameter die Funktion hat.

Nein, es kann sogar sehr davon abhängig sein: bei einer einstelligen 
Funktion kann ein primitiver DT ok sein, bei einer zweistelligen sind 
zwei davon aber meistens nicht gut.

von Rolf M. (rmagnus)


Lesenswert?

Wilhelm M. schrieb:
> Eine header-only-C++-Bibliothek und ein dynamic-link-object sind nunmal
> zwei vollkommen unterschiedliche Sachen.

Ja. Letztere kann ich beispielsweise ohne Neucompilieren des Programms 
austauschen, wenn sie einen Bug hat.

> Aber Du kannst aus einer Header-Only-Bibliothek auch ein dynamic-link-
> object erstellen.

Dann muss ich aber einen Wrapper bauen. Oder wie bekomme ich sonst die 
Header von der Implementation getrennt? Und wie soll das insbesondere 
bei Templates funktionieren?

>> Es ist doch absurd, den Parameter-Typ davon abhängig zu machen, wie
>> viele weitere Parameter die Funktion hat.
>
> Nein, es kann sogar sehr davon abhängig sein: bei einer einstelligen
> Funktion kann ein primitiver DT ok sein, bei einer zweistelligen sind
> zwei davon aber meistens nicht gut.

Das ist deine Sichtweise, aber nicht meine. Ob noch ein zweiter 
Parameter folgt oder nicht, ist für mich nicht die entscheidende Regel 
dafür, von welchem Datentyp der erste Parameter ist.

: Bearbeitet durch User
Bitte melde dich an um einen Beitrag zu schreiben. Anmeldung ist kostenlos und dauert nur eine Minute.
Bestehender Account
Schon ein Account bei Google/GoogleMail? Keine Anmeldung erforderlich!
Mit Google-Account einloggen
Noch kein Account? Hier anmelden.