2

Env: Delphi 2007

<Justification>I tend to use complex records quite frequently as they offer almost all of the advantages of classes but with much simpler handling.</Justification>

Anyhoo, one particularly complex record I have just implemented is trashing memory (later leading to an "Invalid Pointer Operation" error).

This is an example of the memory trashing code:

sSignature := gProfiles.Profile[_stPrimary].Signature.Formatted(True);

On the second time i call it i get "Invalid Pointer Operation"

It works OK if i call it like this:

  AProfile    := gProfiles.Profile[_stPrimary];
  ASignature  := AProfile.Signature;
  sSignature  := ASignature.Formatted(True);

Background Code:

  gProfiles: TProfiles;

  TProfiles = Record
  private
    FPrimaryProfileID: Integer;
    FCachedProfile: TProfile;
    ...
  public
    < much code removed >

    property Profile[ProfileType: TProfileType]: TProfile Read GetProfile;
  end;


  function TProfiles.GetProfile(ProfileType: TProfileType): TProfile;
  begin        
    case ProfileType of
      _stPrimary        : Result := ProfileByID(FPrimaryProfileID);
      ...
    end;
  end;

  function TProfiles.ProfileByID(iID: Integer): TProfile;
  begin
    <snip>
    if LoadProfileOfID(iID, FCachedProfile)  then
    begin
      Result := FCachedProfile;
    end
    else
    ...
  end;


  TProfile = Record
  private     
    ...
  public
    ...
    Signature: TSignature;
    ...
  end;


  TSignature = Record
  private               
  public
    PlainTextFormat : string;
    HTMLFormat      : string;

    // The text to insert into a message when using this profile
    function Formatted(bHTML: boolean): string;
  end;

  function TSignature.Formatted(bHTML: boolean): string;
  begin
    if bHTML then
      result := HTMLFormat
    else
      result := PlainTextFormat;
    < SNIP MUCH CODE >
  end;

OK, so I have a record within a record within a record, which is approaching Inception level confusion and I'm the first to admit is not really a good model. Clearly i am going to have to restructure it. What I would like from you gurus is a better understanding of why it is trashing the memory (something to do with the string object that is created then freed...) so that i can avoid making these kinds of errors in future.

Thanks

Zax
  • 471
  • 1
  • 4
  • 14
  • A property of type record? I'm pretty sure that's not supposed to compile at all. "Invalid pointer operation" means you're freeing something that was already freed, or freeing something that never referred to dynamically allocated memory in the first place. – Rob Kennedy Dec 22 '10 at 05:53
  • @Rob: why shouldn't that compile? Record properties are allowed despite the fact that assignment to record members is not possible (`Obj.Record.Member := 'blub') – jpfollenius Dec 22 '10 at 08:28
  • +1 record properties are perfectly reasonable, for example what about a complex number record which made use of operator overloading? – David Heffernan Dec 22 '10 at 10:45

3 Answers3

9

Your justification for using records over classes seems flawed. Every time you return a record as a function result or pass a record as a function parameter or assign from one record var to another record var, all the fields of that record structure are being copied in memory.

This alone can be cause for concern. Compared to reference types, passing record type variables around can suck the life out of a program. Your code could easily spend more time copying stuff from here to there and there to here than on actually getting work done.

The difference between calling your three functions in series in one statement versus calling the three functions in separate statements is the allocation and lifetime of the intermediate results. When you call the functions in separate statements, you provide local variables to hold the intermediate results between calls. The variables are explicit and their lifetimes are well defined.

When you call the functions in one statement, the compiler is responsible for allocating temporary variables to hold the intermediate results between calls. Lifetime analysis of these implicit variables can become murky - can the same local variable be used to hold the intermediate results of multiple calls in series? Most of the time, the answer is probably yes, but if the record types involved contain fields of compiler-managed data types (strings, variants, and interfaces) the same local variable can't just be overwritten with the next block of data.

Records containing compiler-managed types must be disposed of in an orderly fashion to avoid leaking heap memory. If such a record is overwritten with garbage data, or if such a record is copied without the compiler's awareness, then the compiler generated code to dispose of the compiler-managed fields of the record when the record goes out of scope will likely report that it encountered an invalid pointer and/or a corrupt heap.

Your TSignature record contains string fields, making it a compiler-managed data type. Everywhere that you have a local variable of type TSignature, the compiler has to implicitly generate try..finally frames in the body of the function to make sure the string fields in that local variable structure get released when execution leaves that scope.

Any operation that ends up modifying or overwriting the string field pointers in the TSignature record can lead to an Invalid Pointer Operation error. Making copies of the record (by assigning it to multiple variables) should increment the ref counts automatically, but any use of MemCopy to bulk copy the record's contents to some other location will throw off the ref counts and lead to Invalid Pointer Operation when the cleanup code tries to release those string fields more times than they were actually referenced. Typecasting a record variable to the wrong record type could cause the string fields to be overwritten with garbage, and cause an Invalid Pointer Operation down the line (when the record is cleaned up at the end of the scope)

There is also a possibilty that the compiler itself has lost track of the intermediate record variables in the single-statement scenario and is cleaning up the hidden intermediates too many times or overwriting them without cleaning up the prior values. There was a compiler bug in this area somewhere back in the Delphi 3 era, but I don't recall which product release we fixed it in. I seem to recall the bug I have in mind involved record type function results being passed to const type parameters, so it's not an exact match to your scenario, but the consequences are similar.

Before reporting this as a compiler bug, go over your code with a fine-toothed comb in the debugger disassembly view. There are tons of ways you can screw this up all by yourself. See where the intermediate results are being allocated, written to, and disposed by the compiler-generated code, and how your code is interacting with that pattern.

The smoking gun will be when you see the string fields of one temp record variable overwritten without a call to decrement the reference to those strings. It could be caused by your code, or it could be caused by something in the compiler generated code, but the only way to find out is to witness the act and work out the finger pointing from there.

dthorpe
  • 35,318
  • 5
  • 75
  • 119
  • 1
    Thank you for your detail (I have read it three times now). I am not personally concerned with pointing fingers; only finding a solution. What would you propose as a safer model for a large "class" that requires many sub-classes? – Zax Dec 22 '10 at 04:17
  • 1
    +1 (+10 if I could) for taking the time to explain so thoroughly. – Marjan Venema Dec 22 '10 at 07:43
  • @Xaz: What's the reason you can't use a class to model a "class"? I can't think of a much simpler handling. Is it memory management (really not that complicated when following a clear ownership principle) – jpfollenius Dec 22 '10 at 08:25
  • -1 The Delphi compiler is making a really consistent work about record handling, and managing reference types in records. From the RTL point of view, there is now difference between reference counted types in classes and in records: the same low-level system.pas functions are called. The bug you're referring to is perhaps the multi-threaded related bug of not released string properties (an interlocked DEC was introduced in _LStrClr): but this was affecting both classes and records. The same problems using MemCopy/Move will appear on both records and classes. – Arnaud Bouchez Dec 22 '10 at 08:46
  • @A.Bouchez I hope you are aware that dthorpe used to blog on the Delphi Compiler Core page (http://web.archive.org/web/*/http://blogs.borland.com/dcc); his intimate knowledge of the Delphi compiler was a crucial part of his job. – Jeroen Wiert Pluimers Dec 22 '10 at 11:10
  • @A.Bouchez MemCopy/Move is used way more often for records than for classes, especially because people think they can get away with it (they can only for non compiler-managed records, but often they realize this too late or not at all). That alone could have caused this issue. – Jeroen Wiert Pluimers Dec 22 '10 at 11:11
  • @A.Bouchez Reference counting can cause havoc (will blog about this shortly) when passing stuff around in const parameters; when you don't have local references to the intermediate results, this could have caused the issue at hand. – Jeroen Wiert Pluimers Dec 22 '10 at 11:12
  • @A.Bouchez in stead of a blog entry, I posted a question on the reference counting issue: http://stackoverflow.com/questions/4509015/should-the-compiler-hint-warn-when-passing-object-instances-directly-as-const-int – Jeroen Wiert Pluimers Dec 22 '10 at 12:10
  • 1
    @A.Bouchez: Yes, I'm aware of the interlocked Dec in _LStrClr. I implemented it. ;> – dthorpe Dec 22 '10 at 18:13
  • @A.Bouchez: While you are correct that there is no significant difference to the RTL whether you use compiler-managed types as fields in records or class types, there is a huge difference in the way the user/developer uses record types vs class types. Because the compiler magically takes care of allocation of record types and copying values between them, the developer is more likely to fall into the trap of ignoring the code size and performance costs of records, particularly records containing compiler managed fields. (cont..) – dthorpe Dec 22 '10 at 18:19
  • (...cont) Because instances of class types are passed by reference and assigned by reference, the actual instance data is allocated and freed relatively infrequently compared to the copying of record data. Adding a class type local variable to a procedure will not add any additional code to that procedure; adding a local var of a record type that contains compiler managed fields will add a hidden try..finally block around the body of the procedure to clean up the compiler managed fields in that record. (cont..) – dthorpe Dec 22 '10 at 18:26
  • (...cont) The equivalent code to clean up compiler managed fields in the class type resides in the class's destructor. The difference is that the class destructor is only called once per instance, whereas the record cleanup code is called in every procedure body that contains a variable of that record type. If you use records containing compiler managed fields a lot, this can add up to a silent, invisible performance leech very quickly. – dthorpe Dec 22 '10 at 18:34
  • @Xaz: There's nothing wrong with using records for smallish values, or for chunks of data that are not passed around a lot, but for general operations it's recommended to use a class type for classes instead of records. It's why we created the class type in the first place. ;> To manage memory cleanup of classes, you need to think about lifetimes of the objects and who should be the owner of each object. Look at VCL for guidance - the VCL component ownership model exists primarily to provide automatic memory management for VCL components - without the overhead of garbage collection. – dthorpe Dec 22 '10 at 18:39
  • 1
    @A.Bouchez: I seriously doubt Xaz is running multiple threads here. The Invalid Pointer Operation in this scenario is almost certainly caused by something overwriting the intermediate record value used between the single-statement function calls and trashing the string pointer fields in the Signature record. – dthorpe Dec 22 '10 at 18:47
  • 1
    Well having generated such a lively debate, I believe my work here is done (strides off into the sunset). Seriously though, thanks for everybody's feedback. – Zax Dec 30 '10 at 01:00
  • @dthorpe Thanks for all those comments and knowledge sharing! Records can be evil (slow hidden copy, record hidden slow cleanup code, a fillchar would make any string in record become a memory leak...), but they are sometimes very convenient to access a binary structure (a "smallish value" as you wrote), via a pointer. But using pointers, you must know what you are doing. It's definitively more preferable to stick with classes, and TPersistent if you want to use the magical "VCL component ownership model". I only try to use records if there is no reference counted fields in them. – Arnaud Bouchez Jan 18 '11 at 15:24
2

Its not apparent from the code you supplied where the corruption is happening so here are a few suggestions. Try different combinations of field chaining to see if you can reproduce it.

AProfile := gProfiles.Profile[_stPrimary];
sSignature := AProfile.Signature.Formatted(True);

ASignature := gProfiles.Profile[_stPrimary].Signature;
sSignature := ASignature.Formatted(True);

Turn on range checking and overflow checking if you haven't already. Download FastMM4 and use its FullDebugMode. If none of this leads to an answer learn how to use memory breakpoints.

Kenneth Cochran
  • 11,954
  • 3
  • 52
  • 117
  • Both of these methods work without corruption. It seems that gProfiles.Profile[_stPrimary].Signature is freed after – Zax Dec 22 '10 at 04:00
  • Both of these methods work without corruption. It seems that gProfiles.Profile[_stPrimary].Signature is freed when the method exits after the first call. FullDebugMode did not return anything that was able to help me. – Zax Dec 22 '10 at 04:06
  • Specifically gProfiles.FCachedProfile.Signature is freed after the method exits – Zax Dec 22 '10 at 04:11
  • I think you're missing some point here. Using a method returning a TProfile will make a local copy of the content into the local AProfile variable. Then sSignature can be retrieved from AProfile safely. – Arnaud Bouchez Dec 22 '10 at 06:51
-1

There is something I don't get very well with your code extract. Is TProfile a record? So using a function SomeName: TProfile will make a copy of the record content into the result, which is very inefficient. Even with an optimized version of the record copy function, it's still time consuming.

You should get it by reference/pointer, using a PProfile = ^TProfile type. In this case, you'll prevent most of your memory problems, about accessing string inside a record.

But you should be sure than your original TProfile will remain available in memory, during the whole lifetime of the PProfile pointer.

Using records can be faster/easier than using classes in some (rare) cases, if you parse some binary content for example. But you should never use a plain record type to manipulate records with functions/methods, but a pointer to a record (or a var parameter). It will be both safer and faster.

Arnaud Bouchez
  • 42,305
  • 3
  • 71
  • 159