C++ dll. viele Funktionen vs viele Argumente

T_55

Lieutenant
Registriert
Feb. 2013
Beiträge
638
Hallo Coders,

eine Frage zur Performance beim Aufruf von dll-Funktionen die per call by reference laufen.
Was ist performanter?

1. Eine dll Funktion die 5 Argumente übergibt wo aber nicht jedesmal jedes Argument gebrucht wird:
Code:
void fu(int &a, int &b, int &c, int &d, int &e) {}

oder

2. Die Argumente als Einzelfunktionen entsprechend aufgerufen:
Code:
void fua(int &a) {}
void fub(int &b) {}
void fuc(int &c) {}
void fud(int &d) {}
void fue(int &e) {}


Nehmen wir an im Programm gibt es diverse Aufrufe der Funktion(en) um irgendwas zu machen, jetzt ist die Frage ob man sich das Leben einfach macht und es wie bei 1 macht also all-in-one oder die Argumente mit jeweils einzelnen Funktionen übergibt wie bei 2. Am liebsten wäre mir 1 weil es all-in-one ist und im Zweifel hat man alle Variablen zur Verfügung aber besteht da nicht auch die Gefahr das sich die Performance verschlechtert da auch immer Argumente übergeben werden die nicht zwangsläufig gebruacht werden? Wie machen das die Profis?

Danke schon mal
T_55
 
Die Profis nehmen die zweite Methode.

DLLs sind versionsabhängig. Man ändert dann keine Schnittstellen, sondern definiert neue und die alten Schnittstellen bleiben, wenn möglich, erhalten oder in alten Versionen erhalten.
 
Nehm doch variante 1) aber mit ner default belegung in der funktion. Und im code rufst dann die funktion auf mit dem was fu brauchst.
 
miac schrieb:
Die Profis nehmen die zweite Methode.
DLLs sind versionsabhängig. Man ändert dann keine Schnittstellen, sondern definiert neue und die alten Schnittstellen bleiben, wenn möglich, erhalten oder in alten Versionen erhalten.
Ok was die Updates angeht logisch, wobei für meine simplen Zwecke wahrscheinlich nicht so ausschlaggebend wie es bei einer professionellen Software wäre.

Piak schrieb:
Nehm doch variante 1) aber mit ner default belegung in der funktion. Und im code rufst dann die funktion auf mit dem was fu brauchst.
Wie würde eine default belegung aussehen, geht das in Richtung Struct oder wie kann man sich das vorstellen?


Wie schaut es mit der Performance aus gibt es da Gründe für das eine oder andere?
 
Variante 1 ist schlechter Programmierstil: Du übergibst an eine Funktion Variablen, die diese Funktion nicht benötigt. Damit widersprichst du dem Konzept einer Funktion. Das, was du da veranstalten möchtest ist unnötiges Wirrwarr.

Das ist ein wenig so, als würde ich zu einem Glas Wasser noch Wurst, Käse und ein Kampf-Uboot zur Kasse tragen müssen, weil ich es sonst nicht kaufen dürfte.

Variante 2 ist deutlich sinnvoller, da eine Funktion hier nur die Argumente braucht, die du auch übergibst.


Von der Performance her ist definitiv Variante 2 schneller. Du musst nicht in der Funktion filtern, welche Variablen sinnvoll sind, du provozierst keine unnötigen Sprünge und soweit du sagtest würdest du ohnehin die Funktion nach Variante 1 ohnehin nur aufrufen, um einzelne Werte zu ändern.
Sonderfall: Du bearbeitest überwiegend alle 5 Variablen und nur in sehr sehr seltenen Ausnahmen nur eine Variable. Dann ist Variante 1 schneller, weil du nicht den overhead durch mehrfache Funktionsaufrufe hast.

Die Profis schauen sich an, wie die Daten aussehen, die mit diesen Funktionen verarbeitet werden sollen, und entscheiden danach (sollten sie zumindest). Wobei Variante 1 natürlich elendig zu warten ist. Das ist mehr so eine Wegwerf-Lösung.
 
Danke für die gute Erklärung. Heißt also man sollte bestenfalls immer nur die Variablen in die Funktion geben die auch verarbeitet werden sollen.

Mein Gedanke zu Variante 1, wo natürlich immer wieder unbenötigte Werte vorkommen, war das der call by reference- Aspekt dies kompensiert weil ja nur die Speicheradressen übergeben werden und nicht wie bei call by value echte Kopien stattfinden
 
Naja, dafür berechnest du in Variante 1 bei JEDEM Aufruf ALLE Variablen. Das heißt gemäß dem Minimalfall, dass jede Berechnung nur einer Operation entspricht, verbrennst du in Variante 1 selbst dann 5 Operationen, wenn du nur eine Variable berechnen möchtest.
 
Ja das ist der Punkt, die Speicheradressen zu übergeben sind eben auch Operationen die ins Gewicht fallen und das ist am Ende das Argument für Variante 2 bzw für eine Variante wo nur genau das übergeben wird was auch gebraucht wird.
 
Du musst dir halt überlegen, ob du lieber solchen Code schreibst und v.a. dann auch benutzt...
Code:
void berechneWurzelEinerZahlOderLaengeEinesVektors(float x, float y, bool vektor, bool alsFloat, int& rInt, float& rFloat) {
  float ergebnis;
  if (vektor)
    ergebnis = sqrt(x * x + y * y);
  else
    ergebnis = sqrt(x);  // y wird ignoriert
  if (alsFloat)
    rFloat = ergebnis;  // rInt wird benutzt
  else
    rInt = static_cast<int>(ergebnis);  // rFloat wird nicht benutzt
}

... oder lieber solchen:
Code:
// Casten/Runden/whatever kann der Benutzer auch selbst
float berechneWurzelEinerZahl(float x) { return sqrt(x); }
float berechneLaengeEinesVektors(float y) { return sqrt(x * x + y * y); }

Das Problem ist nicht, dass das Übergeben einer Speicheraddresse lang dauert (das ist im schlimmsten Fall ein zusätzlicher lea-Befehl und ein Store, letzterer fällt in aller Regel sogar weg). Das Problem ist auch nicht die Performance. Das generelle Problem ist, dass du deine Schnittstellen aufweichst und nachher keiner mehr weiß, was die Funktion eigentlich machen soll, und das Problem speziell bei Mutable-Referenzparametern ist, dass du Variablen deklarieren musst, die du nie benutzt.


Aus deinem Beispiel wird leider auch nicht klar, was konkret das eigentlich werden soll, aber es kann durchaus eine legitime Lösung sein, für die gängige Fälle jeweils eine Funktion zu schreiben, auch wenn die dann am Ende nicht viel mehr tun als sich gegenseitig aufzurufen. Allerdings eher selten in Verbindung mit Call-by-Mutable-Reference.
 
Zuletzt bearbeitet:
Variante 2.

Wie Andere schon gesagt haben, ist guter Programmierstil. Auch wenns ein wenig weh tut, sollte man sich angewöhnen.

Wenn du nen Fehler drin hast, ist es da auch leichter zu sehen in welcher Funktion der ist. Du musst dann nicht jedesmal deine Monsterfunktion komplett durchgehen.

Persönlich habe ich nebenbei noch ein testMeinProgramm.cpp auf und schreibe für jeden Fehler den ich finde einen Test. Kann sowas simples sein wie
Code:
std::cout << "MeineFunktion Test1:" << MeineFunktion( x, y, z ) == 23 << std::endl 
              << "Soll: 23 Ist: " << MeineFunktion( x, y, z )  << std::endl;

Hat mir schon Stunden suchen gespart und geht nur gut mit entsprechend kleinen Funktionen.

Performance sollte auch kein Unterschied sein. Kleine Funktionen werden vom Compiler inline gemacht, also kein Sprung mehr.
 
Performance sollte auch kein Unterschied sein. Kleine Funktionen werden vom Compiler inline gemacht, also kein Sprung mehr.
Wobei das natürlich nicht funktioniert, wenn die Funktionen aus ner Library kommen, wie es bei ihm offenbar ja der Fall ist.

An sich aber ein guter Punkt - innerhalb eines Programms bzw. einer Library werden kleine Funktionen natürlich eher inlined als große (sofern die Definition denn auch im Header steht). Wenn der Compiler weiß, was man vor hat, kann er auch besser optimieren - je komplexer die Funktion ist, desto schlechter funktioniert das.
 
wenn die Funktionen aus ner Library kommen

Hast du Recht. Besonders bei ner dll wie bei OP. Bist du dir sicher, dass es bei ner statischen auch nicht passiert? Sollte technisch gehen, kenn mich mit Compilervoodoo aber nicht aus.

sofern die Definition denn auch im Header steht

Wie hier dachte ich immer, dass "inline" eher ne Anfrage an den Compiler ist, als ein Zwang. Der kann das ignorieren oder Funktionen inlinen, die den nicht so definiert sind.
 
Zuletzt bearbeitet: (quote tags)
Der Compiler kann keine Funktion inlinen, deren Definition er nicht kennt. Kennen kann er aber nur die, die in der aktuell compilierten cpp-Datei stehen (in der Regel hat man ja einen Aufruf pro cpp-Datei), sowie inline-Funktionen aus anderen Headern - dazu zählen auch alle nicht-spezialisierten Funktionstemplates sowie Methoden aus nicht spezialisierten Klassentemplates.

Alles andere braucht schon Link Time Optimizations. Dann kann das gehen. Zumindest in den Open Source-Compilern kann man das Feature aber im Moment nicht sinnvoll nutzen.
 
Zuletzt bearbeitet:
Ein kleiner Tipp, wenn du optionale Argumente hast bietet es sich an mit Pointern statt Referenzen zu arbeiten:

Code:
void DoSomething( int32_t* arg1, int32_t* arg2 = nullptr, int32_t* arg3 = nullptr ) 
{
    if ( arg1 )
    {
        // bla bla.
    }

    if ( arg2 )
    {
        // bla bla bla
    }

    if ( arg3 )
    {
        // bla bla bla bla
    }
}

int main()
{
    int32_t v1, v2, v3;
    DoSomething( &v1 );
    DoSomething( &v1, nullptr, &v3 );
}

inline ist eine Anfrage, forceinline eine sehr dringliche Anfrage. Aber auch forceinline kann vom Compiler ignoriert werden, wenn er das für wirklich nötig erarchtet.

Gruß
 
The Ripper schrieb:
Optionale Argumente -> Eine Funktion mit und eine ohne optionale Argument

Das kommt immer auf die Komplexität der Funktion an.

Die Windows API hat auch haufenweise optionale Argumente die dann jeweils als nullptr übergeben werden müssen wenn sie nicht gewünscht sind (RPM als Beispiel, bytesRead kann also DWORD* oder als nullptr angegeben werden).
 
Die Windows-API würde ich aber auch nicht unbedingt mit nem Softwaredesign-Award auszeichnen.

Gerade bei sowas wie deinem Beispiel wäre ein Overload die schönere Lösung, oder aber man zwingt den User gleich, den Wert zu capturen (wenn ich bei der richtigen Funktion gelandet bin, sehe ich keinen Grund, den Wert nicht zurückzuschreiben).

Gibt trotzdem Fälle, wo sowas legitim ist. Viele echte C-APIs arbeiten mit Arrays, die man in der Anwendung selbst anlegen muss. Um die benötigte Länge abzufragen, wird dann gerne mal dieselbe Funktion benutzt wie die, die die Daten dann in das Array schreribt:
Code:
      // Schritt 1: Benötigte Länge ermitteln
      uint32 physicalDeviceCount = 0;
      vk->enumeratePhysicalDevices(vk->instance, &physicalDeviceCount, nullptr);
      
      // Schritt 2: Ein Array anlegen und Daten da reinschreiben
      Array<VkPhysicalDevice> physicalDevices(physicalDeviceCount);
      vk->enumeratePhysicalDevices(vk->instance, &physicalDeviceCount, physicalDevices.data());
      
      // Schritt 3: Wahnsinnig werden, weil die Library es sich nochmal anders überlegt hat
      ASSERT(physicalDevices.size() == physicalDeviceCount);

inline ist eine Anfrage, forceinline eine sehr dringliche Anfrage.
inline ist nicht nur eine Anfrage, inline hat eine fest definierte Semantik. Das soll nicht dem Compiler irgendwas aufzwingen, das soll die Grundvoraussetzungen für Inlining schaffen. Da aber viele Funktionen schon automatisch inline sind, braucht man auch das Keyword fast nie, und AFAIK hat das bei GCC und Clang auch keinerlei Einfluss auf die Optimierung.

Was ist forceinline? Eine Microsoft-Erweiterung?
 
Zuletzt bearbeitet:
Zurück
Oben