Recent

Author Topic: How optimized is the FPC compiler  (Read 23492 times)

PascalDragon

  • Hero Member
  • *****
  • Posts: 3499
  • Compiler Developer
Re: How optimized is the FPC compiler
« Reply #90 on: December 25, 2020, 01:15:19 pm »
I don't care what C++ does. We're talking about Object Pascal here. And in Object Pascal the internal, low level principles between objects and classes are simply different thus they can not be mixed.

Also if you put your objects on the stack in C++ you can't use polymorphism (e.g. declare a type as SomeType, but instantiate a SomeSubType that contains additional fields).

Shpend

  • Full Member
  • ***
  • Posts: 150
Re: How optimized is the FPC compiler
« Reply #91 on: December 25, 2020, 01:18:36 pm »
Ahh okay okay, dont feel botherd pls, I m only wanting to get maximum understanding how fpc does it and what MAYBE can be improved, if not its fine thats what discussions are about, right?

So: Just for my understanding, why cannot object-types inherit then (advanced)record-types? they should kind of share the same memory-layout in Stack, dont they?

Martin_fr

  • Administrator
  • Hero Member
  • *
  • Posts: 7517
  • Debugger - SynEdit - and more
    • wiki
Re: How optimized is the FPC compiler
« Reply #92 on: December 25, 2020, 01:27:24 pm »
They do exist for the case described. What happens outside that described case => not the point.
Then tell me, how should I undertstand this:
Quote
But this inability stems from the way objects are designed [1]. It is part of old style objects.
New style classed to not impose that limit on such abilities.

[1] Under the premise, that no heap is to be used. Which was a given on the original task, and known before you implied that pointers could overcome that limitations.
Because just by reading it looks to me like you are directly comparing the design of old style objects with new style classes. But if you are talking about the specific case described, this comparison makes absolutely no sense, because new style classes are not comparable to that situation at all.

"New style classed to not impose that limit on such abilities."  => To be read as, new style classes force usage of the heap (this is part of the design of new style classes). Therefore they can not experience that limit.

Contrary, old style objects to not force usage on the heap. (Never mind that they could. It is not the point of the overall context that they could. It was said that they should be an the stack)

So I am "comparing"
- new style classes, particularly the fact that their design forces heap usage
versus
- Old style classes, with regards to their design does not force them to be on the heap. And that in this case are chosen to be on the stack

I then ask for how to solve the case of returning an inherited (larger) object.
Using stack only.
Pointer (to stack) as you like.
(Note that when I posted it, I did set the quote markers wrong... / Below code is shortened, follow link for full)
Yes, and my whole argument here is that this has nothing to do with polymorphism.
I think you mean that slightly different (but too complex for me to bother to put in into words).

Because actually it is (afaik) due to polymorphism that the inherited class can have a bigger mem footprint. And that is part of what causes the issue.

But it is not just polymorphism alone.
Then again, I don't think I ever claimed that.

As a general note "design of objects" is more broad than "design of polymorphism in objects".


Maybe I am wrong here, but for my understanding the goal of polymorphism through inheritance as employed in pascal is to allow to access different datatypes through a common interface. The key is the word *accessing* here. Your example is the copying of data, i.e. the storing of data. Polymorphism makes no attempt to unify the storage of data in memory of different objects. And of course it doesn't because there are other concepts for doing exactly this:
In essence, yes like that. But, for polymorphism to work (without restriction as the one starting this argument) the memory model chosen must then have certain properties.

In the given case those properties are not present. Objects allow to chose such an insufficient memory mgmt. (They even default to it). Permitting that choice is part of their design. (not their polymorphism design, but there overall design)

As stated "design of objects" is more broad than "design of polymorphism in objects".
And the (list of) mem mgmt models available is part of the overall design of objects.


Using variant records one can unionize the storage for different objects, and due to the common prefix of  inherited objects this can be used to make use of polymorphism while simultaniously having a unified storage.
But only if you know the size of the biggest possible derived object.
And that size can not be known in all cases (examples given early on in the discussion)

PascalDragon

  • Hero Member
  • *****
  • Posts: 3499
  • Compiler Developer
Re: How optimized is the FPC compiler
« Reply #93 on: December 25, 2020, 03:06:58 pm »
Ahh okay okay, dont feel botherd pls, I m only wanting to get maximum understanding how fpc does it and what MAYBE can be improved, if not its fine thats what discussions are about, right?

We - as in the FPC developers - don't see a need to improve anything here, cause it is fine as it is.

So: Just for my understanding, why cannot object-types inherit then (advanced)record-types? they should kind of share the same memory-layout in Stack, dont they?

Because records are not objects. The compiler handles them completely different.

In addition to that Pascal is about clarity. When I now need to find out whether my parent type is a record or object, what is clear about that?

No, thank you.

Warfley

  • Hero Member
  • *****
  • Posts: 602
Re: How optimized is the FPC compiler
« Reply #94 on: December 25, 2020, 03:56:17 pm »
"New style classed to not impose that limit on such abilities."  => To be read as, new style classes force usage of the heap (this is part of the design of new style classes). Therefore they can not experience that limit.

Contrary, old style objects to not force usage on the heap. (Never mind that they could. It is not the point of the overall context that they could. It was said that they should be an the stack)

So I am "comparing"
- new style classes, particularly the fact that their design forces heap usage
versus
- Old style classes, with regards to their design does not force them to be on the heap. And that in this case are chosen to be on the stack
Now I understand what you mean. You don't compare classes and objects, you compare heap allocation vs stack allocation and just use new style classes as standin for heap allocated instances and old style classes as standin for stack allocated instances.

So basically all you are saying boils down to the following: To allocate memory on the stack the exact type must be known at compile time (modulo inherited types of the same size) while on the heap the type can be deduced during compiletime. Therefore if you want to allocate memory for an object without knowing it's exact type during the writing of your function, stack allocation can not be used and one must resort to using the heap.

And as I already said multiple times, the I completely agree with argument between heap and stack. I just could not see why one would talk about classes and objects and use them as standinds for heap and stack.

I think you mean that slightly different (but too complex for me to bother to put in into words).

Because actually it is (afaik) due to polymorphism that the inherited class can have a bigger mem footprint. And that is part of what causes the issue.

But it is not just polymorphism alone.
Then again, I don't think I ever claimed that.

As a general note "design of objects" is more broad than "design of polymorphism in objects".
As I said, to me, polymorphism only describes the access not the allocation of objects.  Therefore I have never seen that as a polymorphism issue. As an example, polymorphism was used prior to OOP by having structs/records with a comon prefix. This is very often employed in the linux kernel, where structs are used and the pointer they are accessed by, only refers to a type covering the first few fields while the rest was ommited and only used internally. These datatstructures of course always need to be passed by reference as their size is unknown to the outside.
So this whole issue is not an issue of polymorphism, because polymorphism, at least to my understanding, describes the access through an already existing reference. Therefore polymorphism "requires" copy by reference, so the fact that copy by value does not work is not a limitation of polymorphism, it is a limitation of the memory model that is not intendet to be solved by polymorphism.

To the contrary, even in other memory models, polymorphism does not concern itself with memory allocation. Even with classes, the allocation is not polymorphic. You can't allocate a TStringlist by calling a constructor of TStrings. Polymorphism can only be used after the type specific constructor/allocator was called.

So while you can say it's a limit to polymorphism in static memory, because you can't do polymorphism without having memory allocated, it is not a limit of polymorphism in static memory. Because how you allocate memory is not an issue polymorphism is designed to solve.

So copy by value is something I simply do not expect to work for polymorphic types, because this is something polymorphism does not try to address. Personally I think that even this:
Code: Pascal  [Select][+][-]
  1. TBase=object;
  2. TChild=object(TBase); // No extra data
  3.  
  4. function Child: TChild;
  5. [...]
  6.  
  7. var
  8.   b: TBase;
  9. begin
  10.   b := Child;
  11. end;
should not be allowed because copy by value of two different types should not be allowed.

But only if you know the size of the biggest possible derived object.
And that size can not be known in all cases (examples given early on in the discussion)
Well as pascal is a statically typed language, so one could write a preprocessor that automatically generates a type where all derivates of the superclass fit in, as their sizes need to be known during compiletime. But this would be just a form of hack. So yes, if you want to use static memory like the stack, the size of the object needs to be static and known at compiletime, or to be more precise, needs to be known at the time the function is written.

Warfley

  • Hero Member
  • *****
  • Posts: 602
Re: How optimized is the FPC compiler
« Reply #95 on: December 25, 2020, 04:34:19 pm »
@PascalDragon
Pls read my post again (I have eddited cpl of times) how does C++ do all that within stack-area and get away with it??
Pretty simple, it's a seperation of concerns. You can put classes wherever you want, heap, stack, data, etc. But you can't simply put everything on the stack and expect it to work. You must choose the correct allocation method is for the data at hand.

In C++ you need to think about what you need, the data segment is a static memory with global lifetime, this means placing an object here will ensure that it will never be removed from memory, but at compile time it must be known what objects are placed here (as each requires it's own global variable).
The stack is a semi static memory with limited lifetime. Objects placed here will be gone as soon as the function returns. Also most objects on here are known during compiletime, but using alloca and VLAs you can actually allocate memory dynamically on the current stack frame, something fpc does simply not support. But still you are restricted to allocating memory on the current stack frame, i.e. a function can only dynamically allocate memory on the stack that is life during it's lifetime. The heap is dynamically and manually managed, meaning the memory get's allocated and freed when you request it (smart pointer/reference count can automate this) and lives as long as you wish.

So for your classes it does not matter in C++ where they are placed, but you need to know how you want to use them and decide for this where they should be placed.
For example if the exact type and size of data you allocate is not known previously, the data segment is off the table, because here you need to define at compile time which objects reside here. The stack, while being inconvinient can handle dynamic allocations using alloca, and the heap is free for all.
If your data should survive the return of a function, the stack is off the table, data and heap can easiely survive as long as the program runs.
If you need to allocate a lot of data, but don't want to waste memory all the time, i.e. free as soon as possible, the data segment is off the table as it lives from start to finish, and the stack might also be off the table, because the current function might live longer than the individual objects allocated here.

The heap is always the *safe* solution, as it has pretty much no restrictions. The problem here is, that you need to manually allocate and free the data, as well as that heap allocation is pretty slow (compared to alloca where dynamic allocation on the stack is pretty much just a single subtraction of the stack pointer). This is why many high level languages only use the heap. Sure it adds overhead, but as long as you don't care for every bit of performance, it is the safe alternative which gives you the most freedom.
There are other options like a stack allocator placed in the heap, but thats a whole different topic.

It should be noted that while alloca allows for dynamic allocation on the stack, it is really messy and should be avoided if not neccessary.

So to summarize, in C++ it just allows you to do more, but you need to know the limitations of each of your options. The stack is no penecia, you can't just place everything on the stack and think that it will work. Each allocation methods has it's advantages and limitations, and a big part of C++ learning, where beginners often struggle with, is learning what to use when and how

Shpend

  • Full Member
  • ***
  • Posts: 150
Re: How optimized is the FPC compiler
« Reply #96 on: December 25, 2020, 08:17:31 pm »
@Warfley

And to which extent is FPC different to C++ in terms of MemoryModel? (no sarcastic comment!)

Like what are there more of limitations which cant be just added, like I have hard times really understanding that objects are not capable of ineriting records, where da hell is the difference, like they can both be placed on stack, both can have functions/procs, both can contain pointer to other data, both cannot implement interfaces the ONLY difference is that object can inherit other objects thats it, for some reason, im sry @PascalDragon i cannott believe that they are soooooo much different in terms of implementation.. if this is the only thing they are different of.

But tbh, despite the fact that objects do currently not allow to be used as management operators (which doesnt make sense for me either, thw same optimization rules work for records as for them) they are kind of like C++, actually abuit better, since u have the more convient way of saying, i want my data on heap with non-pointer-semantics (aka classes) and on the other hand, you have the ability to have objects on the stack if that makes sense for ur application, (objects(if  polymorphism needed))/records)

Awkward

  • Jr. Member
  • **
  • Posts: 95
Re: How optimized is the FPC compiler
« Reply #97 on: December 25, 2020, 09:19:49 pm »
Shpend, you forgot one detail when comparing records with objects: by nature, objects can have virtual methods, not only direct inheritance. and vice versa, records can have helpers (still don't know why it not exists for objects)

Shpend

  • Full Member
  • ***
  • Posts: 150
Re: How optimized is the FPC compiler
« Reply #98 on: December 25, 2020, 09:45:11 pm »
Yea even that, @Akward. IMHO, objects are abit less powerful classes for the stack(disregarding the heated conversation between @Warfley and @Martin_Fr, I am just simply sayingthey are more welcomed to the stack than anything else :-D to keep it simple) but to write a Fazit, they now only need to have the ability to be extended thru helper (as you stated) and the entire managementstuff (i would love the move-semantics to find its way for records and objects then) and you hacve basically IMHO a better memory model than C++. I personally find that tho..

nanobit

  • Full Member
  • ***
  • Posts: 112
Re: How optimized is the FPC compiler
« Reply #99 on: December 25, 2020, 10:00:46 pm »
I am just simply saying they are more welcomed to the stack than anything else

objects are for stack and heap memory, just like records do:
a: array of TSomeObject
a: array of TSomeRecord
setLength(a, count);

Shpend

  • Full Member
  • ***
  • Posts: 150
Re: How optimized is the FPC compiler
« Reply #100 on: December 25, 2020, 10:02:24 pm »
@Warfley

And to which extent is FPC different to C++ in terms of MemoryModel? (no sarcastic comment!)

Like what are there more of limitations which cant be just added, like I have hard times really understanding that objects are not capable of ineriting records..
To that I wanna add, that internally, the objects just get what a record already have, its fields and methods, thats it, it doesnt need to bother5 for any VMT (from the record i mean which it inherits of) or anything else regarding Auto-Refcounting, since the record doesnt allow interfaces aswell(which makes sense) and so it shouldnt be much of a trouble to allow to just extend the record(s) its inheriting from.

lucamar

  • Hero Member
  • *****
  • Posts: 4219
Re: How optimized is the FPC compiler
« Reply #101 on: December 25, 2020, 11:23:40 pm »
To that I wanna add, that internally, the objects just get what a record already have, its fields and methods, thats it, it doesnt need to bother5 for any VMT (from the record i mean which it inherits of) or anything else regarding Auto-Refcounting, since the record doesnt allow interfaces aswell(which makes sense) and so it shouldnt be much of a trouble to allow to just extend the record(s) its inheriting from.

It's just the other way around: (advanced) records are now getting some features from objects (mainly methods) but, because the lack of inheritance (and other limitations, like lack of virtual methods) they can have a much simpler implementation, without VMTs, etc. than old-style objects.

Old-style objects were an evolution of records, but of the simple, standard records; and were born as a way to introduce OOP into Pascal; as such, they needed a full new way to implement them, much more as they themselves evolutioned, allowing public/private sections, virtual and static methods, etc. which made them incompatible with ... let's call them "old-style" records.

Besides that, they fill, along with classes, conceptually close but different programming "niches" and their respective internal implementations are very different, due in part to this and to their different history.

Advanced records, in turn, were born of the realization that with a few, relatively simple extensions one could get some of the vaunted advantages of OOP without, in fact, using OOP at all. So they have little to do with objects, much less classses, other than a similar sintax.
« Last Edit: December 25, 2020, 11:31:51 pm by lucamar »
Turbo Pascal 3 CP/M - Amstrad PCW 8256 (512 KB !!!) :P
Lazarus/FPC 2.0.8/3.0.4 & 2.0.12/3.2.0 - 32/64 bits on:
(K|L|X)Ubuntu 12..18, Windows XP, 7, 10 and various DOSes.

Warfley

  • Hero Member
  • *****
  • Posts: 602
Re: How optimized is the FPC compiler
« Reply #102 on: December 26, 2020, 05:00:40 am »
And to which extent is FPC different to C++ in terms of MemoryModel? (no sarcastic comment!)

Like what are there more of limitations which cant be just added, like I have hard times really understanding that objects are not capable of ineriting records, where da hell is the difference, like they can both be placed on stack, both can have functions/procs, both can contain pointer to other data, both cannot implement interfaces the ONLY difference is that object can inherit other objects thats it, for some reason, im sry @PascalDragon i cannott believe that they are soooooo much different in terms of implementation.. if this is the only thing they are different of.

There is one limitation that Pascal through the structure of the language has, that C++ simply does not have with regards to the memory model, and this is scoping.
Take this for example:
Code: C  [Select][+][-]
  1. void Foo() {
  2.   // A lot of code
  3.   {
  4.     SomeObject obj;
  5.     // Some code
  6.   }
  7.   // A lot of code
  8. }
obj's lifetime is restricted to the block it is located in, i.e. from the point of where it is defined, to the } of the block it is defined in. In pascal every variables lifetime is always the whole function it is defined in.
This has some non-trivial implications. C++ automatically calls the destructor, as well as, if no other constructor is explicetly used, also the constructor that takes no arguments. Meaning the destructor in C++ and the no argument constructor are comparable to the management operators in advanced records.
This allows for the following constructs:
Code: C  [Select][+][-]
  1. void foo() {
  2.   {
  3.     std::ofstream file_stream("foo.txt");
  4.     file_stream << "foo\n";
  5.   }
  6.   // a lot of code
  7. }
The constructor here opens the file foo.txt, and the destructor automatically closes it when the } is reached. This means the file is closed during the "a lot of code" section.
In pascal this would not be possible this way, because the lifetime of a variable ends with the end of the function, if the same mechanism would be used (i.e. management operators), the file would be kept open during all of the other code section.
That of course gives C++ in that regard a lot more control over the lifetime of objects. Another thing is the initialization/constructor:
Code: C  [Select][+][-]
  1. if (condition) {
  2.   SomeObj obj;
  3.   //...
  4. }
In C++ obj would only be initialized if the condition is true. In pascal the object must be initialized when the function starts and finalized when the function returns.

This has some implications. 1. through the plaicing of blocks you can explicetly define when and where the objects initialization and finalization code is gonna be called and 2. you don't need try-finally anymore (in fact try-finally does not exist in C++) as the destructor is called like a finally block.

If you want the same level of control in pascal you can not use management operators, but must resort to manual constructor and destructor calling, like it is done with old style objects:
Code: Pascal  [Select][+][-]
  1. procedure Foo();
  2. var
  3.   file_stream: OStream; // just pretend there is an object or record like this
  4. begin
  5.   // A lot of code
  6.   file_stream.init('foo.txt');
  7.   try
  8.     file_stream.WriteLn('foo');
  9.   finally
  10.     file_stream.close;
  11.   end;
  12.   // A lot of code
  13. end;
And this is the strength of C++ classes and it's memory model. You can archive the same level of control over the lifetime with much less code. Because at this point, where you manually have to call the constructor and destructor, the only advantage a stack object has over a heap based class, is a tiny bit of performance.
Personally I think in most situations clean code is more important than performance. And while C++ has a lot of things that make code really hard to read and understand, it's scope based lifetime of objects is a massive advantage to keeping your code clean. And this is something that is just by the language design never possible in pascal.

That said, often enough I think having objects live to long, or being initialized even if it is not necessary is only a minor drawback in performance. And if the performance does not matter, management operators allow for much cleaner code, as their C++ equivalent. You just loose some lifetime control and performance. But I would argue that most of the time this does not matter.
And in fact, I already build a file stream record type like the C++ example above using management operators. By putting all the file related stuff in it's own function, you still guarantee the file is not open unessecarily long, while simultaniously getting rid of the try-finally block and the manual calling of the destructor, massively cleaning up the code:
Code: Pascal  [Select][+][-]
  1. procedure CopyFile(const source, target: String);
  2. var
  3.   fr: TFileReader;
  4.   fw: TFileWriter;
  5.   line: string;
  6. begin
  7.   fr.Open(source);
  8.   fl.Open(target);
  9.   for line in fr.lines do
  10.     fl.WriteLn(line);
  11. end;
And this is why I love management operators (just an example, I actually implemented a more efficient copyfile function that does not work on a line basis but on a fixed size buffer basis)

About the internal implementation of records and objects. You must see it in a different way, advanced records are, with respect to the lifetime of the pascal language, fairly new. First there were records, then there were objects, then classes and then advanced records.
So while today the functionality of objects and advanced records might be quite similar, they developed completely differently.
I don't know how they are implemented in the FPC, but I can fully imagine that, as at first there was no intention to add features to records, that records and objects are completely differently implemented, and if you start from a different code base, it is going to develop differently in the future.

Also, what you should not forget, there are 2 different features, records can be variant, i.e. have different fields sharing the same memory:
Code: Pascal  [Select][+][-]
  1.   TColorRec = record
  2.   case Boolean of
  3.   True: (color: TColor);
  4.   False: (R, G, B, A: Byte);
  5. end;
  6. ...
  7. colRec: TColorRec;
  8. ...
  9. colRec.R := 255;
  10. colRec.G := 255;
  11. colRec.B := 255;
  12. colRec.A := 0;
  13. Form1.Color := colRec.color
and a "unique" feature (compared to records) of objects is the usage of a virtual method table. So they are inherently different. So honestly, I don't have any doubt when PascalDragon says they are implemented differently that they are. They where historically completely different and it took a long time before they became so similar they are today.
For example, if I remember correctly, even though advanced records where already a thing, it took a while before records got constructors and with them the new(obj, constructor) syntax we know from objects. Originally they were never intendet to be so similar
« Last Edit: December 26, 2020, 05:10:23 am by Warfley »

Shpend

  • Full Member
  • ***
  • Posts: 150
Re: How optimized is the FPC compiler
« Reply #103 on: December 26, 2020, 10:03:28 am »
thanks for the detailed explanation, @Warfley, the thing only is it could be really complete the niche of old-style-objects, when it would have :

  • management operators
  • move semantics
  • type helper

PascalDragon

  • Hero Member
  • *****
  • Posts: 3499
  • Compiler Developer
Re: How optimized is the FPC compiler
« Reply #104 on: December 26, 2020, 10:44:22 am »
Like what are there more of limitations which cant be just added, like I have hard times really understanding that objects are not capable of ineriting records, where da hell is the difference, like they can both be placed on stack, both can have functions/procs, both can contain pointer to other data, both cannot implement interfaces the ONLY difference is that object can inherit other objects thats it, for some reason, im sry @PascalDragon i cannott believe that they are soooooo much different in terms of implementation.. if this is the only thing they are different of.

Believe what you want. I know the compiler's code, you don't.

  • type helper

Support for type helpers for object types is somewhere on my todo list for the sake of completeness.

 

TinyPortal © 2005-2018