Klasse Optimierung Laufzeit und Speicher CPP

OP #7650579
Lesenswert?

Ich will die Laufzeit und den Speicherverbrauch der Funktion einer Klasse optimieren. Im folgenden Beispiel gibt es die Funktion "exec" mit einem Eingang und einem Ausgang. Wird die Ausführungszeit kleiner wenn man für den Eingang den direkten Wert statt des Pointers nimmt?

1
int Var1 = 0;
2
int Var2 = 0;
3

4
class Test
5
{
6
  public:
7

8
    Test(int a)
9
    {
10
      value = a;
11
    }
12

13
    void exec(int *input, int *output)
14
    {
15
      *output = *input + 1;
16
    }
17

18
  private:
19
    int value;
20

21
};
22

23
Test myObject(0);
24

25
void setup()
26
{
27
  Serial.begin(115200);
28
}
29

30
void loop()
31
{
32
  myObject.exec(&Var1, &Var2);
33
  Serial.println(Var2);
34
  Var1 = Var2;
35
  delay(500);
36
}
Persönliche Seite #7650642
Lesenswert?

Christoph M. schrieb:

Wird die Ausführungszeit kleiner wenn man für den Eingang den direkten Wert statt des Pointers nimmt?

Grundsätzlich ja, aber nur minimal.

Christoph M. schrieb:

Der Compiler ist gut. Er scheint die Klasse vollständig wegzuoptimieren

"Klassen" kann man sowieso nicht wegoptimieren, weil "Klassen" nicht im Assemblercode landen; Klassen sind ein abstraktes Konzept, im Assemblercode landen nur Funktionen. In deinem Fall wurde die Funktion inlined und somit der Pointer wegoptimiert; das passiert nicht immer, z.B. wenn die Funktion größer ist und mehrfach aufgerufen wird oder rekursiv ist.

Allerdings: (Kleine) Input-Daten per Pointer übergeben ist relativ sinnlos, für den Leser verwirrend und sollte schon eher vermieden werden. Mindestens mal sollte ein "const" dran. Kleine Output-Daten sollte man auch eher per Rückgabewert zurückgeben, nicht per Pointer.

OP #7650666
Lesenswert?

Ich brauche Klassen mit mehreren Ausgängen:

1
int Var1 = 0;
2
int Var2 = 0;
3
int Var3 = 0;
4
int Var4 = 0;
5

6
class Test
7
{
8
  public:
9

10
    Test(int a)
11
    {
12
      value = a;
13
    }
14

15
    void exec(const int *input, int *outputA, int *outputB)
16
    {
17
      *outputA = *input + 1;
18
      *outputB = *input + 2;
19
    }
20

21
  private:
22
    int value;
23

24
};
25

26
Test X1(0);
27
Test X2(0);
28
Test X3(0);
29

30
void setup()
31
{
32
  Serial.begin(115200);
33
}
34

35
#define Ende 10000000L
36

37
void loop()
38
{
39
  for(uint32_t n=0;n<Ende;n++)
40
  {
41
    X1.exec(&Var1, &Var2,&Var3);
42
    X2.exec(&Var2, &Var3,&Var4);
43
    X3.exec(&Var3, &Var4,&Var1);
44
  }
45
  Serial.println(Var2);
46
}
Persönliche Seite #7650671
Lesenswert?

Christoph M. schrieb:

Ich brauche Klassen mit mehreren Ausgängen:

Dann kann man sich überlegen, ob man einen Ausgang als Rückgabewert nutzen möchte, weil es der "primäre" Ausgang ist o.ä.

Außerdem kannst du Referenzen nutzen:

1
void exec(const int &input, int &outputA, int &outputB)
2
{
3
  outputA = input + 1;
4
  outputB = input + 2;
5
}
6

7
X1.exec(Var1, Var2,Var3);
8
X2.exec(Var2, Var3,Var4);
9
X3.exec(Var3, Var4,Var1);

Die sind sehr ähnlich zu Pointern, signalisieren (dem Leser) aber eindeutig dass es nur ein einzelner Wert (und kein Array) ist, dass sich die Adresse nicht ändert, und dass die Adresse gültig ist (und nicht NULL/nullptr sein kann wie bei Pointern).

#7651603
Lesenswert?

Wenn mehrere Elemente zusammen gehören, faßt man sie gerne als Struct zusammen. Das verbessert einmal die Lesbarkeit und man muß nur einen Pointer auf die Struct übergeben, anstelle haufenweise einzelne Pointer oder einzelne Variablen.

Christoph M. schrieb:

X1.exec(&Var1, &Var2,&Var3); X2.exec(&Var2, &Var3,&Var4); X3.exec(&Var3, &Var4,&Var1);

Das sieht dann allerdings eher nach einem Array aus (mit Wraparound).

#7651720
Lesenswert?

Niklas G. schrieb:

Also, troll woanders.

Der Link ist einfach ein Hinweis, dass die Ausgangsfrage ziemlich getrollt ist. Deswegen war der auch nur als guter Rat gemeint. Wird doch wohl erlaubt sein? Klasse ist einfach nicht der passende Begriff, um ihn auf Lowlevel-Ebene zu untersuchen. Zur Auseinandersetzung mit Klassen ist bei Java ein, wie ich finde, besserer Backround da.

Persönliche Seite #7651727
Lesenswert?

Rbx schrieb:

Der Link ist einfach ein Hinweis, dass die Ausgangsfrage ziemlich getrollt ist.

Die Ausgangsfrage ist offensichtlich einfach nur eine Anfängerfrage.

Rbx schrieb:

Deswegen war der auch nur als guter Rat gemeint

Dann konntest du den guten Rat nicht weniger von-oben-herab verpacken? Insbesondere auch weil

Rbx schrieb:

Klasse ist einfach nicht der passende Begriff, um ihn auf Lowlevel-Ebene zu untersuchen.

bereits erwähnt wurde:

Niklas G. schrieb:

"Klassen" kann man sowieso nicht wegoptimieren, weil "Klassen" nicht im Assemblercode landen

OP #7652482
Lesenswert?

"Klassen" kann man sowieso nicht wegoptimieren, weil "Klassen" nicht im Assemblercode landen

Man kann Klassen allerdings sehr unterschiedlich implementieren. Sie brauchen dann mehr oder weniger Speicher und sind mehr oder weniger effizient. Bei zeitkritischen Systemen gibt es da große Unterschiede.

#7652744
Lesenswert?

Niklas G. schrieb:

Christoph M. schrieb:

Der Compiler ist gut. Er scheint die Klasse vollständig wegzuoptimieren

"Klassen" kann man sowieso nicht wegoptimieren, weil "Klassen" nicht im Assemblercode landen;

Anführungszeichen braucht man nicht.

Klassen sind ein abstraktes Konzept, im Assemblercode landen nur Funktionen.

Eigentlich gibt es auch "Funktionen" in dem Sinne nicht. Sowas wie Funktionsparameter oder einen Returnwert kennt Assembler nicht, geschweige denn Exceptions und diverse andere Konzepte, die C++ bei Funktionen kennt. Es gibt nur den Sprung zu einem Ziel und den Rücksprung, der automatisch zur ursprünglichen Stelle springt.

In deinem Fall wurde die Funktion inlined und somit der Pointer wegoptimiert; das passiert nicht immer, z.B. wenn die Funktion größer ist und mehrfach aufgerufen wird oder rekursiv ist.

Oder wenn der Compiler den Inhalt der Funktion an der Stelle nicht kennt.

Allerdings: (Kleine) Input-Daten per Pointer übergeben ist relativ sinnlos, für den Leser verwirrend und sollte schon eher vermieden werden. Mindestens mal sollte ein "const" dran.

Eigentlich sehe ich bei solchen Daten nur gerade dann einen Sinn in einem Zeiger, wenn sie nicht const sind, so dass die Funktion da was reinschreiben kann.

Kleine Output-Daten sollte man auch eher per Rückgabewert zurückgeben, nicht per Pointer.

Die Pointer-Variante wird gerne verwendet, wenn mehrere Sachen auf einmal zurückgegeben werden müssen. Allerdings geht das in modernem C++ auch leicht über den Returnwert, ohne dass man dazu extra gleich einen eigenen Typ definieren muss:

1
std::tuple<double, double> Point::coordinates() const
2
{
3
    return { x, y };
4
}
5

6
    auto [ x, y ] = my_point.coordinates();
#7652973
Lesenswert?

Rolf M. schrieb:

Es gibt nur den Sprung zu einem Ziel und den Rücksprung, der automatisch zur ursprünglichen Stelle springt.

Naja, nicht ganz. Man hat auch Übergabeparameter, Registersicherungen,-Wiederhervorholungen, oder auch Rückgabeparameter drin, wenn man möchte. Gruppengeschichten kann man über Moduleinrichtung organisieren, Auto-Geschichten u.a. über die beim Intel (Index-Register). Beim mc muss man sich sowas eher selber einrichten. Je nachdem, was die Hardware so im Angebot hat. Außerdem da auch Möglichkeiten, zu entscheiden, ob man die Rechnerei mehr über die Register laufen lassen möchte, oder mehr lieber mit viel Speicherzugriffen, die aber üblicherweise Zeit kosten. Müsste man sich aber im Einzelfall genauer ansehen. Mit Generalisierungen kann man hier kaum einen Blumentopf gewinnen.

OP #7653030
Lesenswert?

Rolf M. (rmagnus)

Eigentlich sehe ich bei solchen Daten nur gerade dann einen Sinn in einem Zeiger, wenn sie nicht const sind, so dass die Funktion da was reinschreiben kann.

1
void exec(const int &input, int &outputA, int &outputB)

Ich hoffe, das "const" bezieht sich auf den Zeiger und nicht auf den Inhalt, auf den er zeigt.

Persönliche Seite #7653037
Lesenswert?

Christoph M. schrieb:

Ich hoffe, das "const" bezieht sich auf den Zeiger und nicht auf den Inhalt, auf den er zeigt.

Das sind Referenzen und nicht Zeiger, und die Referenz selber ist immer "konstant", also man kann die Adresse nicht ändern (anders als beim Pointer). Somit ist "const int&" immer eine (automatisch konstante) Referenz auf einen konstanten Integer.

  • "const int*" - Nicht-konstanter Zeiger auf konstantes int

  • "int* const" - Konstanter Zeiger auf nicht-konstantes int

  • "const int* const" - Konstanter Zeiger auf konstantes int

  • "int&" - (Automatisch konstante) Referenz auf nicht-konstantes int

  • "const int&" - (Automatisch konstante) Referenz auf konstantes int

Persönliche Seite #7653197
Lesenswert?

Niklas G. schrieb:

1
> void exec(const int &input, int &outputA, int &outputB)
2
> {
3
>   outputA = input + 1;
4
>   outputB = input + 2;
5
> }

Reflexartig Referenzen zu verwenden is m.E. keine gute Idee.

Besser:

1
void exec (int input, int &outputA, int &outputB)
2
{
3
   outputA = input + 1;
4
   outputB = input + 2;
5
}

Erklärung:

"input" belegt nur 2 Bytes und wird von exec nicht geändert, man kann es also ebenso gut per Value übergeben.

Wenn exec geinlined wird, vertut man sich nix und der erzeugte Code ist vermutlich der gleiche (modulo Compiler-Quirx), der C++ Code liest sich aber leichter.

Wenn exec jedoch nicht geinlined wird, ist const int &input teuer, weil (effektiv) die Adresse einer Variablen übergeben wird, und um die Adresse eines auto-Objekts nehmen zu können, muss es im Speicher liegen; hier also im Frame der Funktion. Und das ist wirklich teuer: Zum einen muss ein Frame angelegt werden, das Objekt darin gespeicher werden (auch Konstanten!), zum anderen muss die Adresse berechnet werden. Und der Callee muss derefernzieren, was ebenfalls teuer ist: Adress-Register und RAM-Zugriff werden erforderlich.

Selbst wenn input ein 32-Bit Wert ist, dürfte es effizienter sein, per Value zu übergeben statt per Referenz.

Für output-Operanden gilt evtl. ähnliches: ein int besser als Rückgabewert statt als Referenz. Oder gar ein struct mit 2 ints (kann avr-gcc in Registern halten).

#7653701
Lesenswert?

Rbx schrieb:

Rolf M. schrieb:

Es gibt nur den Sprung zu einem Ziel und den Rücksprung, der automatisch zur ursprünglichen Stelle springt.

Naja, nicht ganz. Man hat auch Übergabeparameter, Registersicherungen,-Wiederhervorholungen, oder auch Rückgabeparameter drin, wenn man möchte.

Bei welcher Architektur? In der Regel gibt es sowas auf Assenbler-Ebene nicht. Man muss das selber z.B. auf Basis von Registern oder dem Stack nachbilden.

Gruppengeschichten kann man über Moduleinrichtung organisieren, Auto-Geschichten u.a. über die beim Intel (Index-Register).

Ich weiß nicht genau, was du damit meinst.

Außerdem da auch Möglichkeiten, zu entscheiden, ob man die Rechnerei mehr über die Register laufen lassen möchte, oder mehr lieber mit viel Speicherzugriffen, die aber üblicherweise Zeit kosten.

In der Regel will man sie so viel wie möglich über Register machen, eben weil die Speicherzugriffe zusätzliche Zeit kosten und sonst eigentlich keinen Vorteil haben.

Antwort schreiben

Bitte melde dich an, um einen Beitrag zu schreiben.

oder

Mit Google-Account einloggen

Die Registrierung ist kostenlos und dauert nur eine Minute.

Jetzt registrieren