Delphi-PRAXiS

Delphi-PRAXiS (https://www.delphipraxis.net/forum.php)
-   Programmieren allgemein (https://www.delphipraxis.net/40-programmieren-allgemein/)
-   -   Delphi Memory Error Detected: TArray<System.WideString> im Record im Record im Byte-Array (https://www.delphipraxis.net/201659-memory-error-detected-tarray-system-widestring-im-record-im-record-im-byte-array.html)

TiGü 12. Aug 2019 14:54

Memory Error Detected: TArray<System.WideString> im Record im Record im Byte-Array
 
Hallo Gemeinde,

Vorgeschichte:
Ich bin gerade dabei im jahre alten Quelltexten Sachen zu optimieren. Delphi-Version ist XE5.
Ein Kernstück des vorhandenen Frameworks basiert darauf, sich komplexe Result-Werte als records quer durchs Programm und auch über Modulgrenzen hinweg zu schicken.

Optimierungsbedarf:
Für String-Informationen wurden dafür fixe/statische Char-Arrays verwendet, stellenweise 1000 bis 2000 Zeichen groß, obwohl in 99 % der Fälle viel weniger reichen würden.

Lösungsidee:
Meine Idee ist, diese statischen Char-Arrays mit dem System.WideString (BSTR) zu ersetzen.
So kann das auch über Modulgrenzen hinweg genutzt werden, die Speicherverwaltung der Strings macht Windows und man hat viel weniger eigentlichen Speicherbedarf, da die Strings halt nur so lang sind, wie man reinsteckt.

Problem:
Es gibt ein generisches Container-Record, in das per eigenen Implicit-Operator die - ich sag mal Sub-Records - transformiert werden können.
Dieses Container-Record speichert die Sub-Records als Byte-Array.
Dabei hat mir FastMM ein Problem aufgezeigt.
Ich habe das Problem anhand eines minimalen Beispiels mit Kommentaren skizziert.
Anschließend befindet sich die Meldung vom externen FastMM (ja, den mit extra DLL).
Ich weiß, dass mein Problem die Sache mit den Record im Record ist, weil so die Referenzzählung vom WideString-Array beim Move kaputt geht.
Aber ich bin gerade so vernagelt und finde keine Lösung.
Hat jemand eine Idee, wie ich die Idee in diesem Rahmen lösen kann?

Delphi-Quellcode:
program MinimalExample;

{$APPTYPE CONSOLE}

{$R *.res}


uses
    FastMM4,
    System.SysUtils;

type
    TContainer = record
        ExternalData: TBytes;
    end;

    TElementA = record
        MyStrings: TArray<Widestring>;
        MyNumber: UInt64;
    end;

    TElementB = record
    public
      type
        TElementBData = record
            A: TElementA;
        end;
    public
        Data: TElementBData;
    end;

procedure Main;
var
    A: TElementA;
    B, B2: TElementB;
    Container: TContainer;
    I: Integer;
    CopyLength: Integer;
begin
    // TElementA zum Leben erwecken und füllen
    A := System.Default(TElementA);
    SetLength(A.MyStrings, 5);
    for I := System.Low(A.MyStrings) to System.High(A.MyStrings) do
    begin
        A.MyStrings[I] := Format('%d%d%d', [I, I, I]);
    end;

    // TElementA dem TElementB.Data unterschieben. ASM: call @CopyRecord.
    // Im echten Quelltext ein Implicit-Operator an TElementB dran.
    B := System.Default(TElementB);
    B.Data.A := A;

    // Container-Record, was die Daten von TElementB in einen Byte-Array halten soll.
    // Im echten Quelltext eine externe Funktion an TContainer, der ich sozusagen nur Pointer und Größe auf B.Data gebe.
    Container := System.Default(TContainer);
    CopyLength := SizeOf(B.Data);
    SetLength(Container.ExternalData, CopyLength);
    // ---> Das Move zerstört wahrscheinlich den RefCount vom B.Data.A.Strings-Array
    Move((@B.Data)^, Container.ExternalData[0], CopyLength);

    // Zurückumwandeln von Container-Element
    // Im echten Quelltext auch wieder ein Implicit-Operator an TElementB dran.
    B2 := System.Default(TElementB);
    // ---> Das Move kopiert zwar den Pointer vom Array, so das B2.Data.A.Strings gefüllt ist
    // ---> aber hinterher FastMM zurecht sagt, dass man dafür in die Delphi-Hölle kommt.
    CopyLength := Length(Container.ExternalData);
    Move(Container.ExternalData[0], B2.Data, CopyLength);

    for I := System.Low(B2.Data.A.MyStrings) to System.High(B2.Data.A.MyStrings) do
    begin
        Writeln(B2.Data.A.MyStrings[I]);
    end;
end;

begin
    Main;
end.
Code:
---------------------------
MinimalExample.exe: Memory Error Detected
---------------------------
FastMM has detected an error during a free block scan operation. FastMM detected that a block has been modified after being freed.

Modified byte offsets (and lengths): 0(1)

The previous block size was: 28

This block was previously allocated by thread 0x36E0, and the stack trace (return addresses) at the time was:
4041F9 [System.pas][System][@ReallocMem$qqrrpvi][4508]
407A0D [System.pas][System][DynArraySetLength$qqrrpvpvipi][33677]
407B3E [System.pas][System][@DynArraySetLength$qqrv][33756]
41DEED [MinimalExample.dpr][MinimalExample][Main$qqrv][42]
406498 [System.pas][System][InitUnits$qqrv][21918]
406504 [System.pas][System][@StartExe$qqrp23System.PackageInfoTablep17System.TLibModule][22052]
4203F5 
771A6359 [BaseThreadInitThunk]
77B67A94 [RtlGetAppContainerNamedObjectPath]
77B67A64 [RtlGetAppContainerNamedObjectPath]


The allocation number was: 113


The block was previously freed by thread 0x36E0, and the stack trace (return addresses) at the time was:
407B7A [System.pas][System][@DynArrayClear$qqrrpvpv][33939]
4075DE [System.pas][System][@FinalizeArray$qqrpvt1ui][31219]
4074BD [System.pas][System][@FinalizeRecord$qqrpvt1][30891]
4075BD [System.pas][System][@FinalizeArray$qqrpvt1ui][31178]
4074BD [System.pas][System][@FinalizeRecord$qqrpvt1][30891]
4075BD [System.pas][System][@FinalizeArray$qqrpvt1ui][31178]
4074BD [System.pas][System][@FinalizeRecord$qqrpvt1][30891]
4075BD [System.pas][System][@FinalizeArray$qqrpvt1ui][31178]
41E04F [MinimalExample][Main$qqrv]
4203F5 
771A6359 [BaseThreadInitThunk]


The current thread ID is 0x36E0, and the stack trace (return addresses) leading to this error is:
40D780 [FastMM4.pas][FastMM4][CheckBlocksOnShutdown$qqro][11424]
40E7C4 [FastMM4.pas][FastMM4][FinalizeMemoryManager$qqrv][12966]
40E830 [FastMM4.pas][FastMM4][Finalization$qqrv][13065]
40642C [System.pas][System][FinalizeUnits$qqrv][21786]
406826 [System.pas][System][@Halt0$qqrv][23185]
4203FA
771A6359 [BaseThreadInitThunk]
77B67A94 [RtlGetAppContainerNamedObjectPath]
77B67A64 [RtlGetAppContainerNamedObjectPath]



Current memory dump of 256 bytes starting at pointer address 7F93B690:
87 92 42 00 80 80 80 80 80 80 80 80 80 80 80 80 80 80 80 80 80 80 80 80 80 80 80 80 2C A7 7C 9E
80 80 80 80 80 80 80 80 00 00 00 00 11 B6 93 7F 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
7C 00 00 00 F9 41 40 00 0D 7A 40 00 3E 7B 40 00 AB DF 41 00 F5 03 42 00 59 63 1A 77 94 7A B6 77
64 7A B6 77 00 00 00 00 00 00 00 00 00 00 00 00 E0 36 00 00 E0 36 00 00 A6 41 40 00 7A 7B 40 00
DE 75 40 00 BD 74 40 00 6B E0 41 00 F5 03 42 00 59 63 1A 77 94 7A B6 77 64 7A B6 77 00 00 00 00
00 00 00 00 18 00 00 00 00 00 00 00 B5 7C 6D 4F 88 92 42 00 80 80 80 80 80 80 80 80 80 80 80 80
80 80 80 80 80 80 80 80 4A 83 92 B0 80 80 80 80 80 80 80 80 80 80 80 80 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
‡  ’  B . €  €  €  €  €  €  €  €  €  €  €  €  €  €  €  €  €  €  €  €  €  €  €  €  , §  |  ž
€  €  €  €  €  €  €  €  . . . . . ¶  “    . . . . . . . . . . . . . . . .
|  . . . ù  A @  . . z @  . > {  @  . «  ß  A . õ  . B . Y c . w ”  z ¶  w
d z ¶  w . . . . . . . . . . . . à  6  . . à  6  . . ¦  A @  . z {  @  .
Þ  u @  . ½  t @  . k à  A . õ  . B . Y c . w ”  z ¶  w d z ¶  w . . . .
. . . . . . . . . . . . µ  |  m O ˆ  ’  B . €  €  €  €  €  €  €  €  €  €  €  €
€  €  €  €  €  €  €  €  J ƒ  ’  °  €  €  €  €  €  €  €  €  €  €  €  €  . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

---------------------------
OK  
---------------------------

Uwe Raabe 12. Aug 2019 15:36

AW: Memory Error Detected: TArray<System.WideString> im Record im Record im Byte-Arra
 
Dir ist aber schon bewusst, daß ein
Delphi-Quellcode:
TArray<Widestring>
nur ein Pointer auf mehrere Pointer zu je einem array of WideChar ist? Das
Delphi-Quellcode:
Move
kopiert aber nur den Pointer des
Delphi-Quellcode:
TArray<WideString>
und nicht die dahinter liegenden Inhalte. Ich glaube kaum, daß dieses Verhalten das gewünschte ist.

peterbelow 12. Aug 2019 15:48

AW: Memory Error Detected: TArray<System.WideString> im Record im Record im Byte-Arra
 
Zitat:

Zitat von TiGü (Beitrag 1440982)
Hallo Gemeinde,

Vorgeschichte:
Ich bin gerade dabei im jahre alten Quelltexten Sachen zu optimieren. Delphi-Version ist XE5.
Ein Kernstück des vorhandenen Frameworks basiert darauf, sich komplexe Result-Werte als records quer durchs Programm und auch über Modulgrenzen hinweg zu schicken.

Optimierungsbedarf:
Für String-Informationen wurden dafür fixe/statische Char-Arrays verwendet, stellenweise 1000 bis 2000 Zeichen groß, obwohl in 99 % der Fälle viel weniger reichen würden.

Lösungsidee:
Meine Idee ist, diese statischen Char-Arrays mit dem System.WideString (BSTR) zu ersetzen.
So kann das auch über Modulgrenzen hinweg genutzt werden, die Speicherverwaltung der Strings macht Windows und man hat viel weniger eigentlichen Speicherbedarf, da die Strings halt nur so lang sind, wie man reinsteckt.

Problem:
Es gibt ein generisches Container-Record, in das per eigenen Implicit-Operator die - ich sag mal Sub-Records - transformiert werden können.
Dieses Container-Record speichert die Sub-Records als Byte-Array.
Dabei hat mir FastMM ein Problem aufgezeigt.
Ich habe das Problem anhand eines minimalen Beispiels mit Kommentaren skizziert.
Anschließend befindet sich die Meldung vom externen FastMM (ja, den mit extra DLL).
Ich weiß, dass mein Problem die Sache mit den Record im Record ist, weil so die Referenzzählung vom WideString-Array beim Move kaputt geht.
Aber ich bin gerade so vernagelt und finde keine Lösung.
Hat jemand eine Idee, wie ich die Idee in diesem Rahmen lösen kann?

Du hast da einen Denkfehler drin. Deine alten array [0..x] of char felder sind value-typen, können also ohne Probleme von speicher zu speicher kopiert werden. Ein TArray<Widestring> ist aber ein referenz-Typ, genau wie Widestring selbst.


Delphi-Quellcode:
type
    TContainer = record
        ExternalData: TBytes;
    end;

    TElementA = record
        MyStrings: TArray<Widestring>;
        MyNumber: UInt64;
    end;

    TElementB = record
    public
      type
        TElementBData = record
            A: TElementA;
        end;
    public
        Data: TElementBData;
    end;
   --snip--
    B.Data.A := A;

    // Container-Record, was die Daten von TElementB in einen Byte-Array halten soll.
    // Im echten Quelltext eine externe Funktion an TContainer, der ich sozusagen nur Pointer und Größe auf B.Data gebe.
    Container := System.Default(TContainer);
    CopyLength := SizeOf(B.Data);
Und da knalls schon, sizeof(B.Data) == sizeof(TElementA) == sizeof(pointer)+sizeof(uint64).

D. h. die Widestrings in dem MYStrings array sind selbst garnicht mitgezählt. Was Du dann kopierst ist die Addresse des Arrays, nicht sein Inhalt. Und den kannst Du auch garnicht blind kopierene, da der Array ja auch nur Referenzen enthält, nicht direkt die zu kopierenden Zeichen.

Du brauchst für jeden Record-Typ Methoden, die den Inhalt in einen Byte-Array kopieren (streamen) und daraus wieder restaurieren können. Das geht nicht wirklich generisch, obwohl man da unter Verwendung von RTTI ziemlich weit kommen kann. Ich weis nicht, wie gut das Marshalling für JSON Support bei XE5 schon war, aber das macht was ähnliches.

TiGü 12. Aug 2019 15:52

AW: Memory Error Detected: TArray<System.WideString> im Record im Record im Byte-Arra
 
Ja eben, im Prinzip müsste man die Zeile mit Move mit irgendetwas kluges ersetzen, ohne das es allzu langsamer ist.

Vorher, mit den festen String-Arrays, war das mit dem Move natürlich nicht so das Problem, dafür waren die resultierenden Records teilweise über 4000 Byte groß, obwohl davon nur ganz wenig gebraucht wurde.

Ich arbeite behelfsmäßig mit einen fixen Array (array[0..19] of Widestring), das knallt dann zumindest nicht sofort.
Das ist aber nur ein Workaround und nicht die endgültige Lösung...

peterbelow 12. Aug 2019 16:35

AW: Memory Error Detected: TArray<System.WideString> im Record im Record im Byte-Arra
 
Zitat:

Zitat von TiGü (Beitrag 1440996)
Ja eben, im Prinzip müsste man die Zeile mit Move mit irgendetwas kluges ersetzen, ohne das es allzu langsamer ist.

Vorher, mit den festen String-Arrays, war das mit dem Move natürlich nicht so das Problem, dafür waren die resultierenden Records teilweise über 4000 Byte groß, obwohl davon nur ganz wenig gebraucht wurde.

Ich arbeite behelfsmäßig mit einen fixen Array (array[0..19] of Widestring), das knallt dann zumindest nicht sofort.
Das ist aber nur ein Workaround und nicht die endgültige Lösung...

Auch den kannst Du nicht einfach per Move kopieren!


Alle Zeitangaben in WEZ +1. Es ist jetzt 07:09 Uhr.

Powered by vBulletin® Copyright ©2000 - 2025, Jelsoft Enterprises Ltd.
LinkBacks Enabled by vBSEO © 2011, Crawlability, Inc.
Delphi-PRAXiS (c) 2002 - 2023 by Daniel R. Wolf, 2024-2025 by Thomas Breitkreuz