Jump to content
Mike Torrettinni

I don't understand this CONST record argument behavior

Recommended Posts

I assumed that using const with arguments to methods is best because it reduces the need for safe handling of arguments/prevents copying... But here I have example that the method is still 'safe handling' the const record:

 

In this example the @InitializeRecord is executed at begin, and @FinalizeRecord at end of method:

function CheckSubItem(const aSubItem: TSubItem): boolean;
begin
  Result := aSubItem.Name = 'A';
end;

function CheckItem(const aItem: TItem): boolean;
var i: Integer;
begin
  Result := false;
  if aItem.SubItems <> nil then
  for i := 0 to Pred(aItem.SubItems.Count) do
  begin
    If CheckSubItem(aItem.SubItems[i]) then // ** THIS CALL ADDS SAFETY HANDLING!?
      Result := True;
  end;
end;

if I have call to CheckSubItem, the CPU view shows:

 

image.png.3b9b9a9ecb9c8bde4f89d231870b6d34.png

 

image.png.8d7d41461a11b216d14a900195ef8d03.png

 

But if I comment the call to CheckSubItem, this this the CPU view for begin/end:

 

image.png.df464c8b339b95711250b3feab90d064.png

 

image.png.90773ae3f3b40f69b36416410d1014b2.png

 

Why are there  @InitializeRecord and @FinalizeRecord if const is used in both methods. Shouldn't there be none of these calls?

 

 

Share this post


Link to post

Does 

aItem.SubItems[i]

access an array directly, or is it a property getter with a function for the getter. The latter means a copy and would explain the behaviour. And if so nothing to do with the const arg. 

Share this post


Link to post
1 minute ago, David Heffernan said:

Does 


aItem.SubItems[i]

access an array directly, or is it a property getter with a function for the getter. The latter means a copy and would explain the behaviour. And if so nothing to do with the const arg. 

No, simple records. Here is the rest of the example details:

 

type
  TSubItem = record
    ID: integer;
    Name: string;
  end;

  TItem = record
    ID: integer;
    Name: string;
    SubItems: TList<TSubItem>;
  end;
      
...
      
procedure TForm2.FormCreate(Sender: TObject);
var
  vItem: TItem;
  vSubItem: TSubItem;
begin
  // test data
  vItem.SubItems := TList<TSubItem>.Create;
  vSubItem.Name := 'A';
  vItem.SubItems.Add(vSubItem);

  if CheckItem(vItem) then
    showmessage('A found');
end;

 

Share this post


Link to post

Wrong. The answer to my question is yes. 

SubItems: TList<TSubItem>

When you access the items, a function getter is called, and that's where the copy comes from. That function getter has to assign its result somewhere and the compiler makes a temporary local variable for it. Hence the record init etc. 

 

Access the array directly and there will be no copy.

aItem.SubItems.List[i]

The const arg works as you expect. 

Edited by David Heffernan
  • Like 2
  • Thanks 1

Share this post


Link to post
2 minutes ago, David Heffernan said:

Wrong. The answer to my question is yes. 


SubItems: TList<TSubItem>

When you access the items, a function getter is called, and that's where the copy comes from. Access the array directly and there will be no copy. The const arg works as you expect. 

You are right! If I do this:

If CheckSubItem(aItem.SubItems.List[i]) then

there is no Initialize/FinalizeRecord anymore.

 

Such a hidden little detail, important detail 🙂

 

  • Like 1

Share this post


Link to post

I reviewed my code using TList and I used .List only when changing the content. All works good until big data set comes into play and then using non-direct access can be a real bottleneck.

This also applies to the simpler TList<string> not only for TList<record>.

 

So, from now on I will always use direct access through .List.

 

Share this post


Link to post
14 minutes ago, Mike Torrettinni said:

So, from now on I will always use direct access through .List.

Probably a bad idea. I would only do this in specific instances where you have timed your program and identified a bottleneck. 

  • Like 2

Share this post


Link to post
1 minute ago, David Heffernan said:

Probably a bad idea. I would only do this in specific instances where you have timed your program and identified a bottleneck. 

What would be a situation where accessing through .List is not advisable?

I assume if I access TList, I control the content anyway, so I assume .List should be safe in all cases, no?

Share this post


Link to post
28 minutes ago, Mike Torrettinni said:

What would be a situation where accessing through .List is not advisable?

I assume if I access TList, I control the content anyway, so I assume .List should be safe in all cases, no?

Access through Items (GetItem) implements range check and throws exception if index is out of range. Using List gives you direct access to underlying dynamic array (which capacity may be larger than number of items) and there is no range check in place. 

 

If range checking does not bother you - because you know that you are looping through correct index range, then you can safely use List.

  • Thanks 1

Share this post


Link to post
On 10/3/2020 at 9:19 AM, Dalija Prasnikar said:

Access through Items (GetItem) implements range check and throws exception if index is out of range. Using List gives you direct access to underlying dynamic array (which capacity may be larger than number of items) and there is no range check in place. 

 

If range checking does not bother you - because you know that you are looping through correct index range, then you can safely use List.

Thanks, yes, in this case I always access valid index.

Share this post


Link to post
2 minutes ago, Mike Torrettinni said:

Thanks, yes, in this case I always access valid index.

Famous last woAccess Violation at address $00000000 ...

  • Haha 5

Share this post


Link to post
2 hours ago, Mike Torrettinni said:

Thanks, yes, in this case I always access valid index.

Nobody ever sets out to access out of bounds. But it happens. It doesn't happen because you decided to have a go at accessing out of bounds in case you get away with it. It happens because it's a mistake.

 

The point of range checking is to detect those programming errors before the user is exposed to them. Presumably you use range checking? 

Share this post


Link to post
8 minutes ago, David Heffernan said:

Nobody ever sets out to access out of bounds. But it happens. It doesn't happen because you decided to have a go at accessing out of bounds in case you get away with it. It happens because it's a mistake.

 

The point of range checking is to detect those programming errors before the user is exposed to them. Presumably you use range checking? 

In for loop, I was sure we don't need range checking, right? Of course when iteration is from 0 to Count-1 index, for TList.

But, when you are iterating through custom indexes, I assume range checking would be needed - but I don't use TList that way - I have some example for arrays, which first makes sure FromIndex and ToIndex are within Low and High of array.

 

Share this post


Link to post
34 minutes ago, Mike Torrettinni said:

In for loop, I was sure we don't need range checking, right? Of course when iteration is from 0 to Count-1 index, for TList.

But, when you are iterating through custom indexes, I assume range checking would be needed - but I don't use TList that way - I have some example for arrays, which first makes sure FromIndex and ToIndex are within Low and High of array.

 

Er, I mean the compiler option. You just switch it on and it finds defects in your code. 

Share this post


Link to post
3 minutes ago, David Heffernan said:

Er, I mean the compiler option. You just switch it on and it finds defects in your code. 

Aha, yes, it's on.

Share this post


Link to post
7 hours ago, Mike Torrettinni said:

Aha, yes, it's on.

It won't fully help you with accessing TList List property. Since List can have more items than actual number of items stored, you can access index at the end of array that will not trigger compiler range error, but you will be accessing items outside of TList Items range.

Share this post


Link to post
10 minutes ago, Dalija Prasnikar said:

It won't fully help you with accessing TList List property. Since List can have more items than actual number of items stored, you can access index at the end of array that will not trigger compiler range error, but you will be accessing items outside of TList Items range.

Which is why accessing via Items is to be preferred unless there are very well understood extenuating circumstances, for instance, measurable and significant performance gains. 

 

But in such a case I'd probably look to use a different collection. 

Share this post


Link to post
45 minutes ago, David Heffernan said:

Which is why accessing via Items is to be preferred unless there are very well understood extenuating circumstances, for instance, measurable and significant performance gains. 

 

I think you are blowing this out of proportion. Understanding that accessing underlying array does not have range check error implemented is not nuclear physics. Nor is making sure that you don't screw up your indexes. If you expect some piece of code needs to be performant for any reason (called a lot, or operating on large data sets) you don't have to really measure every two lines of code to know how to make that part of code run faster.

 

45 minutes ago, David Heffernan said:

But in such a case I'd probably look to use a different collection. 

Why?

 

I know that TList is not the most brilliant piece of code out there, but it is not that bad either. And iterating through dynamic array (List property) will not be any faster if you use some other collection.

Share this post


Link to post
2 hours ago, Dalija Prasnikar said:

I think you are blowing this out of proportion.

I think OP's takeaway was to use direct array access always. I'd say use it with specific intent only. 

 

3 hours ago, Dalija Prasnikar said:

Why?

I'm dead against TList exposing its internal implementation details like that. 

 

Anyway, the Delphi collection classes have been such a mess over the years with so many bugs I gave up on them a long time ago and use my own collection library. If you can't do that then it's hard to see past spring's collections. 

  • Like 1

Share this post


Link to post
20 minutes ago, David Heffernan said:

I think OP's takeaway was to use direct array access always. I'd say use it with specific intent only. 

 

I agree with specific intent part. Not necessarily with always measuring (but this has tendency to push discussion in wrong direction). Measuring performance bottlenecks is important, but once you know that particular construct can cause bottleneck, you might want to avoid using it in performance critical code, even though you will not exactly measure how much of bottleneck that construct really is in that particular code. In other words, life is too short to measure everything, sometimes you just use what you know is the fastest code (with all downsides considered).

 

I am using Items, when I am iterating over small collections in non-critical code. 

20 minutes ago, David Heffernan said:

I'm dead against TList exposing its internal implementation details like that. 

 

Anyway, the Delphi collection classes have been such a mess over the years with so many bugs I gave up on them a long time ago and use my own collection library. If you can't do that then it's hard to see past spring's collections. 

 

Yes, exposing internals is not the best practice in general. In case of TList it is necessary sacrifice because performance requires it. Why not something else - well you can use something else, but you are out of option when core RTL/VCL/FMX frameworks are concerned.

 

For instance speed optimization of FMX framework described in https://dalijap.blogspot.com/2018/01/optimizing-arc-with-unsafe-references.html includes direct array access and using List property. With that optimization in just few places I got from "it barely scrolls" to "fast scrolling" on mobile applications. Yes, ARC also had huge impact in this case, but even without ARC compiler, you can still get better GUI performance on mobile apps with such optimization, and on low end devices every millisecond counts.

  • Like 2

Share this post


Link to post

As @Dalija Prasnikar explained sometimes you are not after super-duper micro optimization, but if something is standing out you try with simple optimization.

 

Here is example that triggered this topic:

 

I started with For..in loop and accessing data the 'correct way' (without .List), the method took almost 3s:

image.thumb.png.34508a0e1ea0e8ce3b5f28ed27171e53.png

 

I changed to for i := 0 to Count - 1the execution time dropped by 60%:

image.thumb.png.ae05778a7cad45c03b47262b68a78331.png

 

And this is execution time accessing data thought .List: a 99% reduced execution time:

image.thumb.png.6eee0413a2053c30d5cebf81ee1f9afc.png

 

The whole method dropped from almost 20% of whole execution process time, to just 0.15%. I would say this a very valid reason to access items directly, through .List.

 

OK, for context, this only came to light after one of the customer's data set came to be 10x bigger than what was thought was the biggest data set. So, the original method was working good and at acceptable performance, but it failed miserably when 1 customer went rogue.

  • Like 2

Share this post


Link to post
1 hour ago, Mike Torrettinni said:

And this is execution time accessing data thought .List: a 99% reduced execution time:

Well done work !

 

One thing though, as friendly reminder and as rule of thumb, get the Count from the container into local var to be used for the loop, and never use Length(List) in that case or any similar, so you don't have to face very ugly bug, the reason is that many containers grow and pre allocate items so the Length is not accurate. in other words "Length(List) >= Count" always.

  • Thanks 1

Share this post


Link to post
9 hours ago, Kas Ob. said:

Well done work !

 

One thing though, as friendly reminder and as rule of thumb, get the Count from the container into local var to be used for the loop, and never use Length(List) in that case or any similar, so you don't have to face very ugly bug, the reason is that many containers grow and pre allocate items so the Length is not accurate. in other words "Length(List) >= Count" always.

I didn't know you can use Length on TList (actually works on TList.List) - I assume Length(TList.List) = TList.Capacity, right?

I always use Count for TList, Length for Arrays.

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

×