Jump to content
Sign in to follow this  
Mahdi Safsafi

Typed constants in Delphi.

Recommended Posts

Hello there,

In the previous topic about Strange behavior for literals @Lars Fosdal asked himself a very interesting question about typed-constant. I quote :

Quote

For me, it is a bigger challenge that a typed constant is not regarded as an actual constant.

In this topic, I'll try to answer the following questions:

- What is a typed-constant ?

- How internally is handled by the compiler ?

- Why a typed-constant can't act as a compile-time constant ?

- What's the relationship between typed-constant and optimization ?
 

What's a typed constant ?

It is a constant that has a type associated with it.

const NotTypedConstant = StaticExpression;
const TypedConstant : ConstantType = StaticExpression;

// e.g:
const A = 5;          // not typed constant.
const B: Integer = 5; // typed constant.

As you can see, all constants must be initialized with a static expression (an expression that the compiler can evaluate at compile time).

However, no-typed-constant can be used in static expression, but a typed-constant can't !

const
  A = 5;          // Ok.
  B = A + 1;      // Ok.
  C: Integer = B; // Ok.
  D: Integer = C; // Error.

Wondering why ? this is because the compiler is seeing C and D as variables (not static-compile-time constant) and it treats them same way as it does with pure variables !

You may think that this kind of behavior is unacceptable ?  In fact there is a good reason for that !

 

How internally is handled by the compiler ?

Please, consider the following example :
 

// ---------------------------- Unit1 ----------------------------
unit Unit1;

interface

// public constants:
const

  MIN = 0;
  MAX = 1000;
  MyData: array [MIN .. MAX] of Integer = (...); 
  
implementation
  // ...
end.
// ---------------------------- Unit2 ----------------------------

unit Unit2;

interface

implementation

uses Unit1; // using Unit1.

function ValidateData : Boolean;
begin
	var First := MyData[MIN];   // first item.
	var Last  := MyData[MAX];   // last  item. 
	// ...
end;

end.

Would not be just perfect to be able to declare First and Last as constants ?
 

const First: Integer = MyData[MIN];
const Last : Integer = MyData[MAX];
// or 
const First = MyData[MIN];
const Last  = MyData[MAX];

 This is not possible ! because when compiling Unit1, the compiler produce a .dcu file that contains two things : an interface and an implementation (obj) section for Unit1:

// ---------------------------- Unit1.dcu ----------------------------

// Note that a .dcu file is a binary format ... I'm just using text format for demonstration ! 

interface section:

// static-compile-time constant with definition:
// ----------------------------------------------
const MIN = 0;        
const MAX = 1000;     

// constant WITHOUT definition:
// -----------------------------
const MyData: array [MIN .. MAX] of Integer; 

implementation section:

DataSection:

MyData = [0, 1, ...];
...

CodeSection:
...

When compiling Unit2. the compiler sees uses Unit1; and opens Unit1.dcu file and only going to read the interface section. Meaning the compiler only sees the declaration of MyData, but does not know about the data itself (data inside MyData):
 

// unit2:
// ------

// knowing the type and size of MyData is more important than knowing whats inside
var value : Integer := MyData[MAX + 100]; // Error violating MyData bounds.

Now, because the compiler does not know about the data. MyData can't be used in a static expression. However you can query it's type and size:
 

// Unit2

const First = MyData[MIN];    // error. compiler does not know what MyData holds.
const Size  = SizeOf(MyData); // Ok. compiler knows the type and size.

Why a typed-constant can't act as a compile-time constant ?

Now, if the compiler is able to read the implementation section from the .dcu file, it will also able to access/read MyData definition and allows using typed-constant inside a static expression ... and much better, it will do a very good job for optimization (constant folding && constant propagation). Excited ? Don't be ! If it does such a thing... It will certainly come at a cost of compilation time a very long compilation time ! That's because for each unit, the compiler must read both section (interface and implementation) and must process all the data inside the implementation section.
Delphi is a fast compiler and allowing the use of typed-constant inside a static-expression will break that theory.  

What's the relationship between typed-constant and optimization ?
So, Is Delphi typed-constant behavior correct ? From my point of view yes ! 
Is Delphi handling it smoothly ? Absolutely no !!! In fact the compiler does not do any kind of optimization (constant folding & propagation) even if the definition is available (constant declared in the same unit) :

implementation

{$O+}

// this is a private declaration. compiler can access to the definition !
const
  MyData: array [0 .. 3] of Integer = (5, 16, 7, 10);

var
  First: Integer;

initialization

// since it can access to the definition, it can generate much better code for :
// First := MyData[0];
// mov reg, 5 // MyData[0] = 5.
// but it just generated :
// mov reg, [@MyData + offset]

// ...
end.

The sad reason behind the above generated code is that typed-constant in Delphi is volatile !

Improving optimization: 
1) Always declaring ordinal type as a non-typed-constant whenever its possible :
 

const First = ...; // compile time constant.
const Last  = ...; // compile time constant.
const MyData: array [MIN .. MAX] of Integer = (First, ..., Last);

2) Using a Link-Time-Code-Generation (LTCG*): This is some how hard to implement but definitely is the best way to improve optimization one for all ! This gives a full view of the program ... meaning the code-generator will have much opportunity to do function in-lining, constant folding & propagation, ... The good thing, its just an option (so you can just use it when doing a Release build). 

LTCG* = is not supported by Delphi. Perhaps its supported with C++Builder (I'm not sure about that). If anyone has a clean info, please make a comment.

 

Conclusion:

The purpose of typed-constants is to hold a non-ordinal-data(array, record, ...), and a way to share data between units and speed-up compilation-time. For ordinal-type (integer,...), you should always use a non-typed-constant.

 

  • Like 2

Share this post


Link to post

Conclusions seem bogus to me. I don't think the compilation speed is a factor, and there are plenty of times when you use typed constants for integers. 

 

The rules really should be:

 

1. Use true constants if possible. 

2. Otherwise use a typed constant. 

 

The factors that force you to use typed constants are broadly because the type is a complex type which doesn't permit true constants. Or you need to take the address of the constant. 

Share this post


Link to post

 

Quote

I don't think the compilation speed is a factor

If compiler treats typed-constant as constant it will be a factor and I clearly explained that !

Quote

The factors that force you to use typed constants are broadly because the type is a complex type which doesn't permit true constants

That's not true ! they could allow using complex type ... but they just didn't for the reason I explained ... they want a performance compilation-time. FYI, the D language for example allows complex compile time constant !!!

Here is a quick example just to prove that this is possible :

import std;

struct TPoint{
	int left;
	int right;
}

static const TPoint[] points = [{0, 10}, {10, 20}, {30, 40}]; //  static-compile-time constant array [0..2] of TPoint

// compile time if
static if (points[1].left == 10){
	version = TEN; // just like {$define TEN}
}

void main()
{
    version(TEN) // {$IFDEF TEN}
    {
    	writeln("ten");
    }
}

 

Quote

Or you need to take the address of the constant. 

If you need to get an address of a constant, you should probably make it a variable ! Getting and address usually involves pointer manipulation. For me I prefer to use a variable. Constant in Delphi is not immutable.  

Share this post


Link to post
Posted (edited)
14 minutes ago, Leif Uneus said:

Whether a typed constant is immutable or not is controlled by a compiler switch. See http://docwiki.embarcadero.com/RADStudio/en/Writeable_typed_constants_(Delphi)

That's not a true immutability ... because the const keyword only qualifies the variable and not the type !

procedure foo;
const
  a: Integer = 5; 
var 
  P: PInteger;
begin
  P := @a;   // typeof(@a) = typeof(P) ... thats not an immutability !!!!!!!!!!!!!!
  P^ := 3;   // Ok.
end;

 

Edited by Mahdi Safsafi

Share this post


Link to post
1 hour ago, Mahdi Safsafi said:

If compiler treats typed-constant as constant it will be a factor and I clearly explained that !

I didn't see the part where you measured compile times. 

 

1 hour ago, Mahdi Safsafi said:

That's not true!

It is true. We're talking about Delphi. We're not talking about some potential other language.

Share this post


Link to post
1 hour ago, Mahdi Safsafi said:

If you need to get an address of a constant, you should probably make it a variable

The fact that you've never come across a need for taking the address of a constant doesn't mean the need doesn't exist. 

 

How about asking why this need arises rather than thinking you've seen all possible use cases for typed constants.

Share this post


Link to post
Quote

I didn't see the part where you measured compile times. 

You don't need to measure the elapsed time !!! think about it : a typed-constant must be mangled by the compiler and the linker must resolve the symbol !!! you're adding an extra overhead to the linker when just you'have the possibility to write const a = 1!
 

Quote

It is true. We're talking about Delphi. We're not talking about some potential other language.

 

I gave the example just to show you that the reason behind not allowing a true constant for array, record, ... wasn't about the complexity of the type (they can do it at any time ... but they didn't ! not because the type is complex ... after all it's statically initialized ! if the compiler evaluated it ... it can handle it !!!!!!!). Are you convinced ?
 

Quote

The fact that you've never come across a need for taking the address of a constant doesn't mean the need doesn't exist. 

If the Delphi compiler allowed dynamic constant expression ... I may need it. Otherwise why should I bother my self with address of a static constant ? unless as I said you want to use pointer manipulation ! 

Can you give a good example on when using an ordinal-typed-constant is better than just a constant ???
 

Share this post


Link to post
25 minutes ago, Mahdi Safsafi said:

Can you give a good example on when using an ordinal-typed-constant is better than just a constant

As I said, when I need to take the address. An example from my code base is when calling external code, written in Fortran, that expects values to be passed by reference always. 

 

26 minutes ago, Mahdi Safsafi said:

You don't need to measure the elapsed time

You do. If you can't provide a real world example where compilation time is significantly impacted, then I call BS. 

 

27 minutes ago, Mahdi Safsafi said:

I gave the example just to show you that the reason behind not allowing a true constant for array, record, ... wasn't about the complexity of the type (they can do it at any time ... but they didn't ! not because the type is complex ... after all it's statically initialized ! if the compiler evaluated it ... it can handle it !!!!!!!). Are you convinced ?

I'm talking about the language as it stands today. Your original post was too. 

Share this post


Link to post
Quote

You do. If you can't provide a real world example where compilation time is significantly impacted, then I call BS. 

That's prove the lack of your experience toward manual compilation/linking and absolutely you have no idea about how a linker work ! 
I'll just give a short explanation :
When declaring a typed-constant, the compiler must mangle its name (this may slow compilation) so the linker can clearly uses/links the correct symbol: Usually a mangled name = Unit-Name+Symbol-Name+Additional-Information. Whenever the symbol is used, the compiler just emits a blank case ... and adds a new entry in the symbol table !
Now, when linking, the linker must iterates through all the blank cases and makes a fix-up ! Now because a symbol table is stored as a hash table, In many time it just happens that the table contains a collision which will be solved by raw string comparison ... a source for a slow linking time. 

If you just ask any experienced c++ developer (because those people who do much manual linking) about what makes a link time slow. He/She will definitely answer: symbol-table size. Moreover the length of the mangled symbol can play a role too ... there was a study I have read a while ago proving that using short name for template type/variable may improve linker time. Just google , you'll find much topics talking about linker.

Untyped-constants on the other hand do not require mangling or linking (they act like a macro) all handled by the compiler.

Now, do you still believe that you need a test to just prove that compilation/linking of typed-constants is slower against constants ? If so, please I'd like to see from you a real world example !!!

 

Quote

As I said, when I need to take the address. An example from my code base is when calling external code, written in Fortran, that expects values to be passed by reference always. 

I accept your example.

 

Quote

I'm talking about the language as it stands today. Your original post was too. 

That does not change the fact that type complexity is not the reason.

Share this post


Link to post
2 minutes ago, Mahdi Safsafi said:

That's prove the lack of your experience toward manual compilation/linking and absolutely you have no idea about how a linker work

Show me some evidence based on measurements. 

  • Like 2

Share this post


Link to post
7 hours ago, Mahdi Safsafi said:

Conclusion:

The purpose of typed-constants is to hold a non-ordinal-data(array, record, ...), and a way to share data between units and speed-up compilation-time. For ordinal-type (integer,...), you should always use a non-typed-constant.

I always thought that non-typed constants are 'old' type of constants before benefits of typed-constants were obvious and were needed to be implemented.

For integers I have no problem with non-type constant, while for colors (hex numbers) I always have to think do I need to specify constant as TColor or not, so I always need to test what works:

 

const
  cColor1: TColor =  $00FAFAFA;
  cColor2 = $00FAFAFA;

Thanks for detailed explanation!

Share this post


Link to post
Quote

Show me some evidence based on measurements. 

 

Since I gave an explanation, I believe that it's you who should give a test! Anyway, I made a simple Perl-script that generates random constants values and a dummy function that consumes those constants:

unit DummyTypedUnit;

interface

const 
	A0 : Integer = RandomValue;
	...
	AN : Integer = RandomValue;

implementation

end.

// -----------------------------
unit DummyUnTypedUnit;

interface

const 
	A0  = RandomValue;
	...
	AN  = RandomValue;

implementation

end.


program [Un]TypedTest;

{$APPTYPE CONSOLE}
{$R *.res}

uses
  System.SysUtils,
  Dummy[Un]TypedUnit in 'Dummy[Un]TypedUnit.pas';

function Dummy(Input: Integer): Integer;
begin
  Result := 0;
  if Input = A0 then inc (Result);
  // ...
  if Input = AN then inc (Result);
end;

var
  i: Integer;

begin
  Read(i);
  i := Dummy(i);
  WriteLn(IntToStr(i));

end.

For each n time, I compiled both test on x86 and x64:
 

# result ; second time
# n = number of constant
# time in min.sec.ms

# x86:
# ----

n = 100    :
untyped    : 00.01.12
typed      : 00.01.18

n = 1000   :
untyped    : 00.01.14
typed      : 00.01.16

n = 10000  :
untyped    : 00.00.88
typed      : 00.00.82

n = 100000 :
untyped    : 00.05.85
typed      : 00.06.26

#x64:
#----

n = 100    :
untyped    : 00.01.10
typed      : 00.01.09

n = 1000   :
untyped    : 00.01.12
typed      : 00.01.15

n = 10000  :
untyped    : 00.01.56
typed      : 00.02.02

n = 100000 :
untyped    : 00.08.90    ;   00.06.48
typed      : 01.00.95    ;   00.48.15


I did my best to make them run on a fair conditions :
- Declaring constants in the same unit.
- Using a short name for constants.


In a world where typed-constants are declared on different units and mixed with long name/type things may get worse specially for x64 !

Let me know if you need to test yourself, I'll be glade to send script and tests.

 

 

  • Like 2

Share this post


Link to post

That's clearly a deficiency in the x64 compiler implementation since the x86 compiler doesn't suffer the problem. The x86 compiler demonstrates that the difference in performance is not due to a fundamental conceptual hurdle, but a poor implementation. We know that the x64 compiler performs very poorly. 

 

In any case the test is totally unrealistic. You aren't going to encounter this in real world code. 

 

 

Share this post


Link to post
5 hours ago, Mike Torrettinni said:

For integers I have no problem with non-type constant, while for colors (hex numbers)

Colours aren't hex. Hex is just a different way to write an integer, using base 16. 

 

For instance $10=16. 

  • Thanks 1

Share this post


Link to post

From my point of view, typed constants are just initialized variables, not true constants. (Even when I do not change their values in runtime.)

When used in code, regular (untyped) constants may produce faster code:

 

const XTyped : integer = 3;
      xUntyped = 3;

var a: integer;

begin
a:=5*XTyped;
if a>20 then exit;
a:=5*XUntyped;
if a>20 then exit;

 

 

c2.thumb.png.4e5b8d137a4d973b04c38e0b888c3e88.png

Share this post


Link to post
Quote

That's clearly a deficiency in the x64 compiler implementation since the x86 compiler doesn't suffer the problem. The x86 compiler demonstrates that the difference in performance is not due to a fundamental conceptual hurdle, but a poor implementation. We know that the x64 compiler performs very poorly. 

Indeed Delphi x64 compiler has a poor implementation, but that's not the primary reason why it performs slowly than an x86 compiler ! In fact, If you've ever used a toolset other than Delphi, you wan't give such explanation ! you'll just realize that compilerX32/linkerX32 outperforms compilerX64/linkerX64. E.g : MSVC (are MS and other teams implementing a super implementation for x86 and a poor one for x64 too ?????).

The reason behind this is subject to the following(but not limited) :

- x64-Linker uses long type for calculating addresses. (Int64 vs Int32).

- x64-Object file size usually is greater than a comparable x86-object file.

-  x64-Linker does an extra heavy work : fixing/linking exception tables (SEH). 

- Finally, It's not fair to compare x64 vs x86 (different file-format, different arch, different exception mechanism, ...).

- ...

FYI, this is slowly going to change (a lot of efforts are made by clang/gcc/icc) but still an x64 toolset has a lot of challenges.

Quote

In any case the test is totally unrealistic. You aren't going to encounter this in real world code. 

First, even if the test is unrealistic, I want to point you that I did my best to make a very fair test comparison :

- I compiled tests without debug-info ( flavor for typed-constants (TC)).

- Each symbol is used ONLY once ... meaning only one relocation (favor for TC).

- All symbols are declared on the same file (favor for TC).

- No detailed map file (favor for TC).

- I used short name for all symbols (favor for TC).

As you can see, all the above conditions where made in favor of TC (I wanted a true fair conditions !). In a real world, breaking the above rules is very easy and may lead to slow compilation. 

 

Second, I will give you a real world example where a usage of just 32 typed-constant can slow-down compilation :

unit BinarySearch;
interface

const BIT_0: Integer  = 1 shl 0;
// ..
const BIT_31: Integer = 1 shl 31;

function binary_search(value: Integer): Integer;

implementation
// node count > 10K
function binary_search(value: Integer): Integer;
begin
	if( (value and BIT_0) <> 0)then
	begin
		// state0
		if ((value and BIT_X) <> 0) then
		begin
			// state1
			// another if ...
			// terminal node.
		end
	end 
	else
	begin
		// statex
		if ((value and BIT_X) <> 0) then
		begin
			// another if ...
			// terminal node.
		end
	end;
end;	
end.

Here is a sample tree, tree2. The first time I developed the code for pascal, it was looking just like above (the decision behind this was to increase readability/debug). When everything was working perfectly, I decided to replace BIT_X with hex value (I wasn't comfortable seeing unppaded text) ... When I did this I noticed a small improvement on compilation time ... the reason that made me investigate on typed-constant ! It was a small improvement but noticeable !

 

Now, if you still insist, than you should really start giving me a good explanation -as I did- (forget about good explanation, just give an explanation or even come up with just a theory) why a typed-constant does not  add any extra overhead on the compiler/linker ?

  • Like 1

Share this post


Link to post

I forgot to mention an important point that I believe will change your mind about typed-symbols: 

I'll give you a very basic example (and I'm sure you've experienced this before). When you use many dll(s) ... did you notice that the startup of your app has significantly taking additional time ??? that's because the dynamic-linker (a component from the os-loader) is fixing your symbols !

Share this post


Link to post
26 minutes ago, Mahdi Safsafi said:

Now, if you still insist, than you should really start giving me a good explanation -as I did- (forget about good explanation, just give an explanation or even come up with just a theory) why a typed-constant does not  add any extra overhead on the compiler/linker ?

My point is that the difference in compilation time is insignificant, and absolutely should not drive your choices of how to write the code. 

 

11 minutes ago, Mahdi Safsafi said:

When you use many dll(s) ... did you notice that the startup of your app has significantly taking additional time ??? that's because the dynamic-linker (a component from the os-loader) is fixing your symbols !

Certainly for the DLL that I ship, that's not the case. The time spent fixing up symbols isn't great because there aren't that many. 

Share this post


Link to post
2 hours ago, David Heffernan said:

My point is that the difference in compilation time is insignificant, and absolutely should not drive your choices of how to write the code. 

Show me some evidence based on measurements.  :classic_wink:

  • Like 1

Share this post


Link to post

It is such fun watching you. Alas, no popcorn at hand presently.

Share this post


Link to post
Quote

My point is that the difference in compilation time is insignificant, and absolutely should not drive your choices of how to write the code. 

Your point was that typed-constants have no impact on compilation time (you were strongly believing on that, called my explanation BS and asked for tests) !

Share this post


Link to post
41 minutes ago, Mahdi Safsafi said:

Your point was that typed-constants have no impact on compilation time (you were strongly believing on that, called my explanation BS and asked for tests) !

And your code showed that they don't. 

Share this post


Link to post
41 minutes ago, David Heffernan said:

And your code showed that they don't. 

The code showed clearly that they do on x64!!! 

Share this post


Link to post

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
Sign in to follow this  

×