views:

226

answers:

3

I'm trying to find an elegant way to access the fields of some objects in some other part of my program through the use of a record that stores a byte and accesses fields of another record through the use of functions with the same name as the record's fields.

TAilmentP = Record // actually a number but acts like a pointer
private
  Ordinal: Byte;
public
  function Name: String; inline;
  function Description: String; inline;
  class operator Implicit (const Number: Byte): TAilmentP; inline;
End;

 TSkill = Class
   Name: String;
   Power: Word;
   Ailment: TAilmentP;
 End;

class operator TAilmentP.Implicit (const Number: Byte): TAilmentP;
begin
  Result.Ordinal := Number;
  ShowMessage (IntToStr (Integer (@Result))); // for release builds
end;

function StrToAilment (const S: String): TAilmentP; // inside same unit
var i: Byte;
begin
  for i := 0 to Length (Ailments) - 1 do
    if Ailments [i].Name = S then
    begin
      ShowMessage (IntToStr (Integer (@Result))); // for release builds
      Result := i; // uses the Implicit operator
      Exit;
    end;
  raise Exception.Create ('"' + S + '" is not a valid Ailment"');
end;

Now, I was trying to make my life easier by overloading the conversion operator so that when I try to assign a byte to a TAilmentP object, it assigns that to the Ordinal field. However, as I've checked, it seems that this attempt is actually costly in terms of performance since any call to the implicit "operator" will create a new TAilmentP object for the return value, do its business, and then return the value and make a byte-wise copy back into the object that called it, as the addresses differ.

My code calls this method quite a lot, to be honest, and it seems like this is slower than just assigning my value directly to the Ordinal field of my object.

Is there any way to make my program actually assign the value directly to my field through the use of ANY method/function? Even inlining doesn't seem to work. Is there a way to return a reference to a (record) variable, rather than an object itself? Finally (and sorry for being off topic a bit), why is operator overloading done through static functions? Wouldn't making them instance methods make it faster since you can access object fields without dereferencing them? This would really come in handy here and other parts of my code.

[EDIT] This is the assembler code for the Implicit operator with all optimizations on and no debugging features (not even "Debug Information" for breakpoints).

add al, [eax] /* function entry */
push ecx
mov [esp], al /* copies Byte parameter to memory */
mov eax, [esp] /* copies stored Byte back to register; function exit */
pop edx
ret

What's even funnier is that the next function has a mov eax, eax instruction at start-up. Now that looks really useful. :P Oh yeah, and my Implicit operator didn't get inlined either.

I'm pretty much convinced [esp] is the Result variable, as it has a different address than what I'm assigning to. With optimizations off, [esp] is replaced with [ebp-$01] (what I assigning to) and [ebp-$02] (the Byte parameter), one more instruction is added to move [ebp-$02] into AL (which then puts it in [ebp-$01]) and the redundant mov instruction is still there with [epb-$02].

Am I doing something wrong, or does Delphi not have return-value optimizations?

+1  A: 

Delphi is supposed to optimize return assignment by using pointers. This is also true for C++ and other OOP compiled languages. I stopped writing Pascal before operator overloading was introduced, so my knowledge is a bit dated. What follows is what I would try:

What I'm thinking is this... can you create an object on the heap (use New) and pass a pointer back from your "Implicit" method? This should avoid unnecessary overhead, but will cause you to deal with the return value as a pointer. Overload your methods to deal with pointer types?

I'm not sure if you can do it this with the built-in operator overloading. Like I mentioned, overloading is something I wanted in Pascal for nearly a decade and never got to play with. I think it's worth a shot. You might need to accept that you'll must kill your dreams of elegant type casting.

There are some caveats with inlining. You probably already know that the hint is disabled (by default) for debug builds. You need to be in release mode to profile / benchmark or modify your build settings. If you haven't gone into release mode (or altered build settings) yet, it's likely that your inline hints are being ignored.

Be sure to use const to hint the compiler to optimize further. Even if it doesn't work for your case, it's a great practice to get into. Marking what should not change will prevent all kinds of disasters... and additionally give the compiler the opportunity to aggressively optimize.

Man, I wish I know if Delphi allowed cross-unit inlining by now, but I simply don't. Many C++ compilers only inline within the same source code file, unless you put the code in the header (headers have no correlate in Pascal). It's worth a search or two. Try to make inlined functions / methods local to their callers, if you can. It'll at least help compile time, if not more.

All out of ideas. Hopefully, this meandering helps.

Pestilence
Inlining was introduced in Delphi 2005, and it has always been cross-unit. I see nothing in the documentation suggesting that Delphi's `inline` directive is affected by any debugging settings.
Rob Kennedy
Yes, I know about that, but that happens only if you use classes, which are heap-allocated. Also, as far as I remember, C++ classes are stack-allocated, meaning no optimizations for return assignments.I could create a pointer like you say by making TAilmentP a class itself, but then there'd be no point to using the Implicit operator as that class will actually be a pointer instead of "emulating" one.
Cloud737
I've checked and you're right, inlining is disabled in Debug mode (Release mode if fine), though I'm not sure that's the default since I've changed the options in the build configurations, including the Base one. Guess that's why the compiler warned me about inlining problems only in Release mode, though it should've done so before, regardless.Anyway, I've switched to release mode and printed the addresses of the return values (edited my question to include the code), and sure enough they're still different.
Cloud737
Delphi does allow for cross-unit inlining, I've just used it before in this project. There are some restrictions, like not being able to have inlined functions in the interface part that uses inlined functions in the implementaion part, or have inlining between circular units (the ones which use each other in the uses clause), but it generally works well.Btw, I think the Delphi equivalent to headers is the "interface" part of a unit.That said, all the code I posted is located in the same unit.
Cloud737
As for using const parameters, I forgot about that. I usually use it, especially on heap objects (classes).Speaking of which, aren't const parameters actually const references, meaning that they're implemented as pointers in assembler? Wouldn't that add a bit of overhead if all you want to pass is a Byte or SmallInt? Would the optimizations the compiler does compensate for the overhead?
Cloud737
@Rob Kennedy:Pestilence is referring to the fact that "Code Inlining Control" is disabled by default in the Debug configuration, thus the compiler won't inline code in that state.I can attest to that, since the compiler would give me warning about invalid use of inlining only when building in Release mode.
Cloud737
So, in other words, inlining has absolutely nothing to do with what "mode" you're in and everything to do with whether inlining is enabled. The "modes" are just pre-configured groups of settings that happen to have text labels "Release" and "Debug." You're free to change them however you want.
Rob Kennedy
Ah, yes, of course. I was assuming you were referring to the default build configurations when you talked about "debugging settings".In the end, we were all saying the same thing, just that it wasn't that clear to the others.
Cloud737
A: 

Now that I think about it, maybe it's absolutely necessary to have the return value in a different memory space and copied back into the one being assigned to.

I'm thinking of the cases where the return value may need to be de-allocated, like for example calling a function that accepts a TAilmentP parameter with a Byte value... I don't think you can directly assign to the function's parameters since it hasn't been created yet, and fixing that would break the normal and established way of generating function calls in assembler (i.e.: trying to access a parameter's fields before it's created is a no-no, so you have to create that parameter before that, then assign to it what you have to assign OUTSIDE a constructor and then call the function in assembler).

This is especially true and obvious for the other operators (with which you could evaluate expressions and thus need to create temporary objects), just not so much this one since you'd think it's like the assignment operator in other languages (like in C++, which can be an instance member), but it's actually much more than that - it's a constructor as well. For example

procedure ShowAilmentName (Ailment: TAilmentP);
begin
  ShowMessage (Ailment.Name);
end;

[...]
begin
ShowAilmentName (5);
end.

Yes, the implicit operator can do that too, which is quite cool. :D In this case, I'm thinking that 5, like any other Byte, would be converted into a TAilmentP (as in creating a new TAilmentP object based on that Byte) given the implicit operator, the object then being copied byte-wise into the Ailment parameter, then the function body is entered, does it's job and on return the temporary TAilmentP object obtained from conversion is destroyed. This is even more obvious if Ailment would be const, since it would have to be a reference, and constant one too (no modifying after the function was called).

In C++, the assignment operator would have no business with function calls. Instead, one could've used a constructor for TAilmentP which accepts a Byte parameter. The same can be done in Delphi, and I suspect it would take precedence over the implicit operator, however what C++ doesn't support but Delphi does is to have down-conversion to primitive types (Byte, Integer, etc.) since the operators are overloaded using class operators. Thus, a procedure like "procedure ShowAilmentName (Number: Byte);" would never be able to accept a call like "ShowAilmentName (SomeAilment)" in C++, but in Delphi it can.

So, I guess this is a side-effect of the Implicit operator also being like a constructor, and this is necessary since records can not have prototypes (thus you could not convert both one way and the other between two records by just using constructors). Anyone else think this might be the cause?

Cloud737
You are mistaken on several of your points here. C++ *can* "down-convert," as you call it, by providing conversion operators. Delphi *can* convert "both ways" between two record types: http://stackoverflow.com/questions/770809 If you wish to observe when constructors and implicit operators are called, set breakpoints or print trace messages; don't just guess about what would or would not happen. I don't know what you're talking about regarding "directly assigning to a function's parameters."
Rob Kennedy
Hey, thanks! I didn't know C++ had conversion operators as well until now.As for Delphi, I know it can convert both ways, that was basically what I (tried to?) say before.And forget about the rest of the message, that was just my guess at it, since I couldn't see any other answer. I knew it was a bad idea posting it, but hey, I learned C++ has conversion operators! :D
Cloud737
+3  A: 

Return types — even records — that will fit in a register are returned via a register. It's only larger types that are internally transformed into "out" parameters that get passed to the function by reference.

The size of your record is 1. Making a copy of your record is just as fast as making a copy of an ordinary Byte.

The code you've added for observing the addresses of your Result variables is actually hurting the optimizer. If you don't ask for the address of the variable, then the compiler is not required to allocate any memory for it. The variable could exist only in a register. When you ask for the address, the compiler needs to allocate stack memory so that it has an address to give you.

Get rid of your "release mode" code and instead observe the compiler's work in the CPU window. You should be able to observe how your record exists primarily in registers. The Implicit operator might even compile down to a no-op since the input and output registers should both be EAX.


Whether operators are instance methods or static doesn't make much difference, especially not in terms of performance. Instance methods still receive a reference to the instance they're called on. It's just a matter of whether the reference has a name you choose or whether it's called Self and passed implicitly. Although you wouldn't have to write "Self." in front of your field accesses, the Self variable still needs to get dereferenced just like the parameters of a static operator method.

All I'll say about optimizations in other languages is that you should look up the term named return-value optimization, or its abbreviation NRVO. It's been covered on Stack Overflow before. It has nothing to do with inlining.

Rob Kennedy
I've looked into it (even with "Debugging information" off, meaning no breakpoints whatsoever, just to be sure... what a pain that was :P), and I see it still tries to copy something (the result?), though it seems the end result is a rather useless op.Here's the assembler code for the Implicit operator (without any formatting, unfortunately):add al, [eax]; /* function entry */push ecx;mov [esp], al; /* copies Byte parameter to memory */mov eax, [esp]; /* copies stored Byte back to register */pop edx;ret;Maybe the object at ESP is actually the part I'm trying to assign to?
Cloud737
What's even funnier is that the next function has a mov eax, eax instruction at start-up. Now that looks really useful. :POh yeah, and my Implicit operator didn't get inlined either. :(
Cloud737
I know what you mean about the Self variable and dereferencing it. What I was saying was that if Implicit was an instance method, I could've assigned directly to the field I wanted to, instead of assigning to the field of the return value and then having it copied byte by byte to the one assigned.Anyway, I think Delphi actually does return-value optimizations, just that I'm too stupid to see it. I'll just code the way it feels more comfortable to me and let the compiler do it's job without worrying about micro-optimisations, since I'm sure the compiler is smart enough.
Cloud737
The assembler instructions you're citing look pretty naive. Are you sure you have optimization turned on?
Rob Kennedy
Yes, I'm absolutely sure. *checks again* Yup. Even inlining is on.Even more so, all debugging options are turned off. Stack Frames, Assertions, Debug Information, Local Symbols, all off. And I made sure I built the whole program in Release mode before running (as even if switching build configurations, if I don't build the whole program again, it would just compile and run on the previous configuration).
Cloud737
Disabling optimizations doesn't have much of an effect there, just that it replaces [esp] with [ebp-$01] and [ebp-$02]. It still has the useless set of mov x, y; mov y, x; instructions, just that there isn't any movzx anymore, which makes it even more redundant. It seems [esp] is where the Result variable is at, and it gets put back into the EAX register when the function exits (no optimizations).I've also taken my call to the implicit operator out of any loop (be it inside a function or outside of any function, repeatedly calling the function) just to be sure, and the effect is the same.
Cloud737