Is there a benefit in using old style `object` instead of `class` in Delphi?
Asked Answered
B

4

11

In Delphi sane people use a class to define objects.
In Turbo Pascal for Windows we used object and today you can still use object to create an object.

The difference is that a object lives on the stack and a class lives on the heap.
And of course the object is depreciated.

Putting all that aside:

is there a benefit to be had, speed wise by using object instead of class?

I know that object is broken in Delphi 2009, but I've got a special use case1) where speed matters and I'm trying to find if using object will make my thing faster without making it buggy
This code base is in Delphi 7, but I may port it to Delphi 2007, haven't decided yet.


1) Conway's game of life

Long comment
Thanks all for pointing me in the right direction.

Let me explain a bit more. I'm trying to do a faster implementation of hashlife, see also here or here for simple sourcecode

The current record holder is golly, but golly uses a straight translation of Bill Gospher original lisp code (which is brilliant as an algorithm, but not optimized at the micro level at all). Hashlife enables you to calculate a generation in O(log(n)) time.

It does this by using a space/time trade off. And for this reason hashlife needs a lot of memory, gigabytes are not unheard of. In return you can calculate generation 2^128 (340282366920938463463374607431770000000) using generation 2^127 (170141183460469231731687303715880000000) in o(1) time.

Because hashlife needs to compute hashes for all sub-patterns that occur in a larger pattern, allocation of objects needs to be fast.

Here's the solution I've settled upon:

Allocation optimization
I allocate one big block of physical memory (user settable) lets say 512MB. Inside this blob I allocate what I call cheese stacks. This is a normal stack where I push and pop, but a pop can also be from the middle of the stack. If that happens I mark it on the free list (this is a normal stack). When pushing I check the free list first if nothing is free I push as normal. I'll be using records as advised it looks like the solution with the least amount of overhead.

Because of the way hashlife works, very little popping takes place and a lot of pushes. I keep separate stacks for structures of different sizes, making sure to keep memory access aligned on 4/8/16 byte boundaries.

Other optimizations

  • recursion removal
  • cache optimization
  • use of inline
  • precalculation of hashes (akin to rainbow tables)
  • detection of pathological cases and use of fall-back algorithm
  • use of GPU
Bianchi answered 23/5, 2011 at 22:28 Comment(7)
I would consider records instead of old style object type.Zackaryzacks
I believe the question has the answer. With stack-based objects you're saving heap allocation, de-allocation, you have your object with only a stack pointer change.Tana
If you can't run Conway's game of life fast enough with Class based objects, you're doing LOTS of things wrong. I bet you could write the whole thing without even allocating one object on the heap beyond the original set of objects you generated at the start. It would be WRONG for example to allocate an object per square on the board. The board should be represented by a 2d array, which you can wrap in a class, if you like. One instance is the entire "game board". It is THESE kinds of optimizations (decisions) that you need to spend your time on. Forget about OBJECT. It's dead.Rubato
@Warren, I already have an optimal algorithm (hashlife). So I'm done with that stage, please don't assume stuff.Bianchi
Not assuming stuff. I'm making things explicit: Heap allocation of TObjects is going to be negigible element in your performance, or else, you're doing it wrong.Rubato
It's not allocation of memory is 90% of my runtime (currently in c, but because I suck at c I need it rewritten in Delphi so I can rethink the allocations) Memory allocation is a fundamental property of the algorithm, but right now all my time is spend in calloc and malloc. The algorithm does billions of generations per second and it speeds up exponentially the further you go along, but it takes long to start up because it needs to prepare stuff and that's were I want to save time. So I need to get rid of memory allocation completly and just do 1 allocation of 1GB of memory and work with that.Bianchi
Maybe you need to write your own memory pool object then instead of going directly to a heap.Rubato
K
16

For using normal OOP programming, you should always use the class kind. You'll have the most powerful object model in Delphi, including interface and generics (in later Delphi versions).

1. Records, pointers and objects

Records can be evil (slow hidden copy if you forgot to declare a parameter as const, 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 (e.g. some "smallish value"), via a pointer.

A dynamic array of tiny records (e.g. with one integer and one double field) will be much faster than a TList of small classes; with our TDynArray wrapper, you will have high-level access to the records, with serialization, sorting, hashing and such.

If 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".

Inheritance is not allowed for records. You'll need either to use a "variant record" (using the case keyword in its type definition), either use nested records. When using C-like API, you'll sometimes have to use object-oriented structures. Using nested records or variant records is IMHO much less clear than the good old "object" inheritance model.

2. When to use object

But there are some places where objects are a good way of accessing already existing data.

Even the object model is better than the new record model, because it handles simple inheritance.

In a Blog entry last summer, I posted some possibilities to still use objects:

  • A memory mapped file, which I want to parse very quickly: a pointer to such an object is just great, and you still have methods at hand; I use this for TFileHeader or TFileInfo which map the .zip header, in SynZip.pas;

  • A Win32 structure, as defined by a API call, in which I put handy methods for easy access to the data (for that you may use record but if there is some object orientation in the struct - which is very common - you'll have to nest records, which is not the very handy);

  • A temporary structure defined on the stack, just used during a procedure: I use this for TZStream in SynZip.pas, or for our RTTI related classes, which map the Delphi generated RTTI in an Object-Oriented way not as the TypeInfo which is function/procedure oriented. By mapping the RTTI memory content directly, our code is faster than using the new RTTI classes created on the heap. We don't instanciate any memory, which, for an ORM framework like ours, is good for its speed. We need a lot of RTTI info, but we need it quick, we need it directly.

3. How object implementation is broken in modern Delphi

The fact that object is broken in modern Delphi is a shame, IMHO.

Normally, if you define a record on the stack, containing some reference-counted variables (like a string), it will be initialized by some compiler magic code, at the begin level of the method/function:

type TObj = object Int: integer; Str: string; end;
procedure Test;
var O: TObj
begin // here, an _InitializeRecord(@O,TypeInfo(TObj)) call is made
  O.Str := 'test';
  (...)
end;  // here, a _FinalizeRecord(@O,TypeInfo(TObj)) call is made

Those _InitializeRecord and _FinalizeRecord will "prepare" then "release" the O.Str variable.

With Delphi 2010, I found out that sometimes, this _InitializeRecord() was not always made. If the record has only some no public fields, the hidden calls are sometimes not generated by the compiler.

Just build the source again, and there will be...

The only solution I found out was using the record keyword instead of object.

So here is how the resulting code looks like:

/// used to store and retrieve Words in a sorted array
// - is defined either as an object either as a record, due to a bug
// in Delphi 2010 compiler (at least): this structure is not initialized
// if defined as a record on the stack, but will be as an object
TSortedWordArray = {$ifdef UNICODE}record{$else}object{$endif}
public
  Values: TWordDynArray;
  Count: integer;
  /// add a value into the sorted array
  // - return the index of the new inserted value into the Values[] array
  // - return -(foundindex+1) if this value is already in the Values[] array
  function Add(aValue: Word): PtrInt;
  /// return the index if the supplied value in the Values[] array
  // - return -1 if not found
  function IndexOf(aValue: Word): PtrInt; {$ifdef HASINLINE}inline;{$endif}
end;

The {$ifdef UNICODE}record{$else}object{$endif} is awful... but the code generation error didn't occur since..

The resulting modifications in the source code are not huge, but a bit disappointing. I found out that older version of the IDE (e.g. Delphi 6/7) are not able to parse such declaration, so the class hierarchy will be broken in the editor... :(

Backward compatibility should include regression tests. A lot of Delphi users stay to this product because of the existing code. Breaking features are very problematic for the Delphi future, IMHO: if you have to rewrite a lot of code, which shouldn't you just switch the project to C# or Java?

Kurdistan answered 24/5, 2011 at 6:11 Comment(2)
@Bouchez, thanks for a very helpful answer. Object does not seem broken for my use case (no strings), but I'm going with record because I've decided not to use a stack after all.Bianchi
Afaik in Free Pascal they are now initialized.Duplicator
N
7

Object was not the Delphi 1 method of setting up objects; it was the short-lived Turbo Pascal method of setting up objects, which was replaced by the Delphi TObject model in Delphi 1. It was kept around for backwards compatibility, but it should be avoided for a few reasons:

  1. As you noted, it's broken in more recent versions. And AFAIK there are no plans to fix it.
  2. It's a conceptualy wrong object model. The entire point of Object Oriented Programming, the one thing that really distinguishes it from procedural programming, is Liskov substitution (inheritance and polymorphism), and inheritance and value types don't mix.
  3. You lose support for a lot of features that require TObject descendants.
  4. If you really need value types that don't need to be dynamically allocated and initialized, you can use records instead. You can't inherit from them, but you can't do that very well with object either so you're not losing anything here.

As for the rest of the question, there aren't all that many speed benefits. The TObject model is plenty fast, especially if you're using the FastMM memory manager to speed up creation and destruction of objects, and if your objects contain lots of fields they can even be faster than records in a lot of cases, because they're passed by reference and don't have to be copied around for each function call.

Niggard answered 23/5, 2011 at 23:49 Comment(2)
I think that if you put a const for the record/object parameter, its content won't be copied at each function call. It's only if you use a record/object as a function result that the copy will be available. In this case, you'll better use a var parameter instead of a function result when you work with record/object.Kurdistan
I agree with most of what you said, but note than any record or object is also passed by reference. OK, a copy is made in the prolog of the function, if the item is not passed as const.Duple
B
6

When given a choice between "fast and possibly broken" and "fast and correct," always choose the latter.

Old-style objects offer no speed incentive over plain old records, so wherever you might be tempted to use old-style objects, you can use records instead without the risk of having uninitialized compiler-managed types or broken virtual methods. If your version of Delphi doesn't support records with methods, then just use standalone procedures instead.

Bitartrate answered 23/5, 2011 at 23:41 Comment(4)
+1 for Fast and Correct. performance is always a secondary concern. Making a mess quickly, and doing it wrong quickly, are never better than doing it right.Rubato
Problem with record is that it doesn't handle inheritance. A lot of plain public API structures, and even some custom structures will be object oriented, so will need this inheritance. A variant record (with a "case integer") is not very elegant, and nested records are not a good choice either.Kurdistan
@A.Bouchez, inheritance in old-style objects has been buggy since the '90s — in the sense that the compiler silently generates incorrect code. If inelegance is the price for correctly compiled code, I'll pay it every time.Bitartrate
With simple object inheritance, I never had such problems. Virtual methods where buggy AFAIK but for static methods, there never was incorrect code generated. You're right that inelegance is sometimes worth it... in order to circumvent the compiler problems... so we are not to blame in this case! ;)Kurdistan
M
1

Way back in older versions of Delphi which did not support records with methods then using object was the way to get your objects allocated on the stack. Very occasionally that would yield worthwhile performance benefits. Nowadays record is better. The only feature missing from record is the ability to inherit from another record.

You give up a lot when you change from class to record so only consider it if the performance benefits are overwhelming.

Menses answered 24/5, 2011 at 6:36 Comment(6)
@David they are, a naive implementation of hashlife spends 90% of the time in the memory intensive (startup)fase allocating memory.Bianchi
Record inheritance isn't a missing feature, it's a non-feature that would harm things more than it helps. Inheritance and value types don't mix, because you end up with all sorts of parameter passing problems when the derived type adds new fields. Just look at C++'s "copy constructors" to see how ugly this model can get.Niggard
@Mason I don't agree. I do agree that the C++ model is exceedingly complex, but they are able to have stack allocation for their objects (e.g. strings) whilst the rest of us have to use the heap and pay the price. I've used object inheritance quite successfully in a very small number of places.Menses
the price for using the stack for your strings is not always "free" either. TANSTAAFL.Rubato
@warren I guess you give up copy on write.Menses
The funny thing is that with a Delphi string type, you can declare a string nominally-on-your-stack, but the storage isn't on the stack, ergo the variable length. Of course, C++ strings on the stack are immutable too aren't they? The list goes on and on...Rubato

© 2022 - 2024 — McMap. All rights reserved.