11
votes

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
4
I would consider records instead of old style object type.Jørn E. Angeltveit
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.Sertac Akyuz
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.Warren P
@Warren, I already have an optimal algorithm (hashlife). So I'm done with that stage, please don't assume stuff.Johan
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.Warren P

4 Answers

15
votes

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?

7
votes

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.

6
votes

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.

1
votes

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.