staging.inyokaproject.org

Größenveränderung von ausführbaren Dateien

Status: Gelöst | Ubuntu-Version: Ubuntu MATE 20.04 (Focal Fossa)
Antworten |

Dakuan

Avatar von Dakuan

Anmeldungsdatum:
2. November 2004

Beiträge: 6532

Das ist jetzt nicht wirklich ein Problem, aber ich würde gerne die Zusammenhänge kennen.

Situation: Ich habe ein etwas umfangreicheres Projekt, in dem 3 Klassen eine identische Funktion benötigen. Diese hatte ich bisher einfach (identisch) in die neuen Klassen kopiert. Prinzipiell ist das natürlich blöd, weil der Code dann drei mal in der Binärdatei vorhanden ist. So mein Verständnis.

Daher habe ich gerade diese Funktion in eine andere Datei als Funktion mit eigenem namespace ausgelagert.

In den drei Klassen wird diese Funktion insgesamt sieben Mal aufgerufen. Ich hätte jetzt erwartet, dass die Binärdatei etwas kleiner wird, tut sie aber nicht. Die Größe bleibt mit 735096 Bytes gleich.

Liegt das jetzt daran, dass die Funktion sehr klein ist, oder kann der Compiler identische Funktionen in unterschiedlichen Klassen erkennen und eine entsprechende (zusammenfassende) Optimierung durchführen?

Die Frage interessiert mich deshalb, weil ich noch mehrere solche Kandidaten habe.

Die kleine Funktion, mit der ich angefanfen habe ist:

1
2
3
4
#include <time.h>
namespace VtCom {
char *  time2date( time_t * t );
}

und

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
#include <cstdio>
#include "vt_commons.h"
//-----------------------------------------------------------------------------
//      Convert the filetime to a human readable string.
//      The user should copy the result as soon as possible!
char * VtCom::time2date( time_t * t ) {
    static char tstring[24];    // Not thread-save!
    tm *    dt;
    dt = localtime( t );
    snprintf( tstring, sizeof(tstring),
        "%4d-%02d-%02d  %02d:%02d:%02d",
        dt->tm_year + 1900, dt->tm_mon + 1, dt->tm_mday,
        dt->tm_hour, dt->tm_min, dt->tm_sec );
    return tstring;
}

Da frage ich mich natürlich, ob es überhaupt Sinn macht, über solche Zusammenfassungen nachzudenken. Wobei es zumindest auf Quelltextebene möglicherweise etwas übersichtlicher wäre.

Neral

Anmeldungsdatum:
3. Oktober 2007

Beiträge: 230

Allgemein geht das schon (insbesondere wenn man LTO benutzt, damit der Compiler mehrere Compilation Units gleichzeitig sehen kann), aber in diesem konkreten Beispiel ist das IMHO nicht möglich. Der Grund ist sogar schon durch einen Kommentar markiert 😉: Die Funktion hat eine statische Variable, die verändert wird, wodurch die drei Kopien tatsächlich Kopien bleiben müssen, damit sie nicht gleichzeitig auf die gleiche statische Variable zugreifen.

Wenn man das static char tstring[24]; durch ein auto tstring = new char[24]{}; ersetzt (sollte man natürlich nicht machen, nur zu Demozwecken), dann kombiniert GCC die drei Kopien tatsächlich in eine, wenn man -flto benutzt: https://godbolt.org/z/655M14EYc

Im Assembler-Output steht für main:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
main:
 sub    rsp,0x18
 xor    edi,edi
 call   401050 <time@plt>
 lea    rdi,[rsp+0x8]
 mov    QWORD PTR [rsp+0x8],rax
 call   4011a0 <A::time2date(long*) [clone .constprop.0] [clone .isra.0]>
 lea    rdi,[rsp+0x8]
 call   4011a0 <A::time2date(long*) [clone .constprop.0] [clone .isra.0]>
 lea    rdi,[rsp+0x8]
 call   4011a0 <A::time2date(long*) [clone .constprop.0] [clone .isra.0]>
 xor    eax,eax
 add    rsp,0x18
 ret    

Also wird da offensichtlich drei mal die gleiche Funktion aufgerufen.

Dakuan schrieb:

Da frage ich mich natürlich, ob es überhaupt Sinn macht, über solche Zusammenfassungen nachzudenken. Wobei es zumindest auf Quelltextebene möglicherweise etwas übersichtlicher wäre.

Natürlich macht das Sinn. Du schreibst den Code ja nicht für den Compiler, sondern damit du ihn verstehen und in Zukunft ändern kannst. Und kopierter Code ist da immer extrem nervig, weil man – wenn man was ändern will – immer alle Kopien gleichzeitig erwischen muss.

Ich würde diese Zeitformatierung übrigens so schreiben (mit <chrono> aus der Standardbibliothek und {fmt} zum Formatieren):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
#include <chrono>
#include <string>

#include <fmt/core.h>
#include <fmt/chrono.h>

auto time2date(auto time) -> std::string {
    return fmt::format("{:%Y-%m-%d %H:%M:%S}", time);
}

auto main() -> int {
    fmt::print("{}\n", time2date(std::chrono::system_clock::now()));
}

(godbolt)

user_unknown

Avatar von user_unknown

Anmeldungsdatum:
10. August 2005

Beiträge: 17630

Früher (90er) habe ich einiges mit C und C++ gemacht, aber das ist lange her und war fast alles autodidaktisch, so dass mein Wissen nicht sehr tief reicht und möglicherweise veraltet ist.

Aber soweit ich weiß gab es immer und gibt es auch noch Optimierungsschalter, die man ein- und ausschalten kann, bzw. man kann die Aggressivität einstellen, ich mein beim gcc ist das -O (O wie Oh! Optimiere!) gefolgt von einer optionalen Zahl (-O2, -O3, ...) die eine Stufe angibt, d.h. man kann nicht beliebig große Zahlen wählen.

Und dann gibt es einen Zielkonflikt zwischen kurzem Code und schnellem Code, was für mich anfangs überraschend und kontraintuitiv war: Bei Wegen in der Stadt sind kürzere Wege meist auch schnellere. Bei Binärprogrammen ist aber duplizierter Code, der gleich an Ort und Stelle ist, schneller, als ein Funktionsaufruf, der einen Sprung veranlasst und dann zurückspringt länger. Beim Loop-Unrolling werden Sprünge wegoptimiert über künstliche Redundanz.

Man kann das in der IDE oft feineinstellen oder eben auf der Kommandozeile über Schalter und jeder Compiler kann diese Optimierungen nach eigenem Gusto haben oder nicht haben und implementieren.

Ich weiß jetzt nicht, ob das C oder C++ ist und nehme an, Du benutzt gcc, aber manche nutzen ja auch einen Intelcompiler oder etwas drittes, viertes.

Als ich bei mir die Optimierungen auf hoch eingestellt hatte, aber auf Optimierung(Size) (war aber der Borland C Compiler bcc für DOS) war ich überrascht, dass beim Debugging der Code aus einer Funktion raussprang und den Code aus der Mitte einer anderen Methode abarbeitete um dann wieder zurückzuspringen, ohne dass da ein return gewesen wäre. Erst hielt ich alles für außergewöhnlich kaputt und bekloppt, weil das war ja der gleiche Code (2 Zeilen oder so) der da, wo er weggesprungen ist, auch gestanden hat. Aber da dämmerte mir, dass der Debugger mir zeigt, was die Größenoptimierung bewirkt hatte, dass eben identischer Code wegoptimiert wurde.

Später fand ich das dann auch erst bestätigt. Die Doku hatte mich nicht mit der Nase drauf gestoßen, dass Größen- und Speedoptimierung widersprüchliche Zielsetzungen sind.

Ich vermute das ist beim Gcc ähnlich und die Frage wäre damit, ob Du Optimierungsflags gesetzt hast und welche, bzw. was die Defaulteinstellungen sind.

kB Team-Icon

Supporter, Wikiteam
Avatar von kB

Anmeldungsdatum:
4. Oktober 2007

Beiträge: 9837

Dakuan schrieb:

[…] Projekt, in dem 3 Klassen eine identische Funktion benötigen

Du verwendest welche Programmiersprache in welcher Version und welchen Compiler in welcher Version? Und welche Optimierungseinstellungen verwendest Du? Und der Linker/Loader bzw. dessen Konfiguration beeinflusst das Ergebnis ja auch noch maßgeblich.

Dakuan

(Themenstarter)
Avatar von Dakuan

Anmeldungsdatum:
2. November 2004

Beiträge: 6532

Neral schrieb:

... aber in diesem konkreten Beispiel ist das IMHO nicht möglich. Der Grund ist sogar schon durch einen Kommentar markiert 😉: Die Funktion hat eine statische Variable, die verändert wird, wodurch die drei Kopien tatsächlich Kopien bleiben müssen, damit sie nicht gleichzeitig auf die gleiche statische Variable zugreifen.

Klingt einleuchtend.

Dein Beispiel zeigt mir übrigens, dass C++ sich schneller ändert als ich Bücher kaufen kann.

@user_unknown Ich habe mir auch alles selber angelesen und den Borland Compiler hatte ich auch. Möglicherweise liegen auf meinem Dachboden noch ein 3/4 Meter der zugehörigen Dokumentation.

Das mit dem Zielkonflikt lässt mich hier (weitgehend) kalt. Der Benutzer ist hier der Bremser und die GUI kann ich nicht beeinflussen. Die Dateigröße wäre da schon interessanter aber wenn ich mir die Containerformate ansehe, ist das wohl auch uninteressant.

Ich verwende meistens keine Optimierungseinstellung. Ich finde es beim Debuggen nervig, wenn ich mir eine Variable ansehen will, die der Compiler dann schon entsorgt hat.

Ich verwende C++ und g++ und habe an der Konfiguration nichts geändert.

Neral

Anmeldungsdatum:
3. Oktober 2007

Beiträge: 230

Dakuan schrieb:

Dein Beispiel zeigt mir übrigens, dass C++ sich schneller ändert als ich Bücher kaufen kann.

Bis auf das auto für kürzere Templates benutzt das nichts, was nicht mit C++11 geht (und das ist inzwischen 11 Jahre alt):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
#include <chrono>
#include <string>

#include <fmt/core.h>
#include <fmt/chrono.h>

template<typename T>
auto time2date(T const& time) -> std::string {
    return fmt::format("{:%Y-%m-%d %H:%M:%S}", time);
}

auto main() -> int {
    fmt::print("{}\n", time2date(std::chrono::system_clock::now()));
}

(godbolt)

Dakuan

(Themenstarter)
Avatar von Dakuan

Anmeldungsdatum:
2. November 2004

Beiträge: 6532

Ich muss zugeben, dass ich mich bei <chrono> gestern davon habe irritieren lassen, dass einige Neuerungen mit C++20 markiert waren. Das ist mir dann sofort ins Auge gesprungen. Und in dem älteren meiner Bücher, was sich u.a. auf die Neuerungen von C++11 bezieht, sind sogar einige Seite dazu zu finden (habe ich aber erst Heute entdeckt).

Aber zu fmt::format finde ich nur das.

Aber das zeigt, das ich nicht der einzige bin, der mit der traditionellen Art und Formatierung der Textausgaben bei C++ nicht ganz glücklich ist 😉

Antworten |