Jump to content
Sign in to follow this  
Mike Torrettinni

Example of wasteful, innefficient string manipulation

Recommended Posts

I was trying to run an example of wasteful, inefficient string manipulation to see what happens to memory consumption, without Fastmm.

I expected the end result to be high memory consumption, de-fragmented, not leaving much free memory for project to use.

But at the end of parsing the project uses almost the same system memory:

Start: image.png.8d4d426e41fb0d043acd900d7e31be4a.png

End:  image.png.33a196d9eef30fa4570101851489df1c.png

 

During the parsing the memory consumption is all over, the highest over 1GB:

 

image.thumb.png.6c9ce542d91e67c861ba1d111210208e.png

 

Here is example of string manipulation:

 

uses StrUtils;

const cLoop = 100;
      cMultiplyString = 100000;

function MultiplyStr(aStr: string; aMultiplier: integer): string;
var i: Integer;
begin
  for i := 1 to aMultiplier do
    Result := Result + aStr;
end;

function Parse(aString: string): string;
var s1, s2: string;
    i: Integer;
begin
  s1 := MidStr(Copy(MidStr(aString, 1, Length(aString) - 2), 1, Length(aString) - 2), 10, Length(aString) div 2);
  for i := 1 to 100 do
    Delete(s1, 1, 1);
  s2 := s1 + s1;
end;

procedure TForm2.Button1Click(Sender: TObject);
var s: string;
    i: integer;
begin
  for i := 1 to cLoop do
  begin
    s := '';
    s := Parse(MultiplyStr('STRING_TO_PARSE' + i.ToString, cMultiplyString));
  end;
end;

 

Is Delphi's memory manager so good that even if the work is wasteful, inefficient string manipulation, when it ends it cleans up memory very well?

 

  • Haha 1

Share this post


Link to post

Delphi's memory manager is fastmm. Also, your tool to measure the effect of the program isn't really telling you anything. It says nothing about fragmentation of address space.

 

Virtual memory is a very complex subject. I suspect you need to learn more of the details before you can reason about your program. 

  • Thanks 1

Share this post


Link to post
12 minutes ago, Mike Torrettinni said:

Aha, I see that Fastmm was integrated in Delphi in 2007(?), so I don't really need Fastmm in my projects with 10.2.3, I guess.

 

If you want to get the most of FastMM (different configurations for debug/release, fine tuning of settings) you still need the standalone version. The bundled one is a "good middle ground" but there always are options to get more...

  • Like 1

Share this post


Link to post
2 minutes ago, Alexander Elagin said:

If you want to get the most of FastMM (different configurations for debug/release, fine tuning of settings) you still need the standalone version. The bundled one is a "good middle ground" but there always are options to get more...

I do use latest, full version of Fastmm, but am not changing any settings. Still from the times of D7 and D2006, and I never checked if it's still needed. It looks like what is integrated with Delphi works very well on it's own.

Share this post


Link to post

When you ask for memory, MM(FastMM) asks the OS for a large chunks and then it splits them and gives you a piece (based on the size you need). When the object is destroyed (free), the memory is returned to the MM. Now, based on the returned size, the MM may either choose to recycle the object location (if its small) or return the memory to the OS (a real-free-op).

What you're doing in MultiplyStr is not just wasteful but extremely harmful ! For each iteration you're reallocating memory. Allocating a new block and copying the old block to the new one. It's very important to know that small block are implemented as a segregated list. i.e if you ask for a 32 bytes, MM on reality allocates an entire table i.e 32x32=1024 bytes and yields first block. In your example you said you used 1GB ! this is extremely bad because you're not economizing resources and you'll quickly run out of memory i.e another thread that asks for a large chunk.

It's indeed a good practice to pre-allocate memory :

function MultiplyStr(aStr: string; aMultiplier: integer): string;
var i: Integer;
begin
  SetLength(Result, length(aStr) * aMultiplier);
  for i := 1 to aMultiplier do
    Result := Result + aStr;
end;

 Please run the above and notice the memory and performance !!!

Also a small remark ! aStr should be const !!!

Edited by Mahdi Safsafi
  • Thanks 1

Share this post


Link to post
1 minute ago, Mahdi Safsafi said:

When you ask for memory, MM(FastMM) asks the OS for a large chunks and then it splits them and gives you a piece (based on the size you need). When the object is destroyed (free), the memory is returned to the MM. Now, based on the returned size, the MM may either choose to recycle the object location (if its small) or return the memory to the OS (a real-free-op).

What you're doing in MultiplyStr is not just wasteful but extremely harmful ! For each iteration you're reallocating memory. Allocating a new block and copying the old block to the new one. It's very important to know that small block are implemented as a segregated list. i.e if you ask for a 32 bytes, MM on reality allocates an entire table i.e 32x32=1024 bytes. In your example you said you used 1GB ! this is extremely bad because you're not economizing resource and you'll quickly run out of memory i.e another thread that asks for a large chunk.

It's indeed a good practice to pre-allocate memory :


function MultiplyStr(aStr: string; aMultiplier: integer): string;
var i: Integer;
begin
  SetLength(Result, length(aStr) * aMultiplier);
  for i := 1 to aMultiplier do
    Result := Result + aStr;
end;

 Please run the above and notice the memory and performance !!!

Also a small remark ! aStr should be const !!!

Thank you, but this wasteful, inefficient, slow and pretty much very bad example was done on purpose.

I do not have such example in my code, but I do use MidStr, Delete, LeftStr... in some cases, so I wanted to see what happens in very bad example.

 

I just wanted to test what the end result would be if you use such code. Seems like memory manager does pretty good job at the end.

Share this post


Link to post
1 minute ago, Mike Torrettinni said:

I just wanted to test what the end result would be if you use such code. Seems like memory manager does pretty good job at the end.

No! as I told you can quickly run out of memory. Imagine what will happen when you use 1 GB and another thread just reclaimed a big chunk of memory ! out of memory !

Share this post


Link to post
1 minute ago, Mahdi Safsafi said:

No! as I told you can quickly run out of memory. Imagine what will happen when you use 1 GB and another thread just reclaimed a big chunk of memory ! out of memory !

Yes, when preparing this example I got carried away and I got 'out of memory' right away. So I had to scale back the bad example to work as intended 🙂

Share this post


Link to post

Mike, you should always rationalize your resource and don't let someone else do it for you (MM) because this can have a wide effect :

- OS may start to page things.

- All sort of thrashing issues (check the link).

- Cache miss.

- Performance penalty.

- ...

  • Thanks 1

Share this post


Link to post

Interesting! I just ran the same example in D2006 and result is almost the same, at the end of the parsing the memory comes down to almost as it was at the start. Peak memory consumption is very similar, around 1.1GB.

I don't have D7 installed to try.

  • Haha 1

Share this post


Link to post
17 hours ago, Mahdi Safsafi said:

When you ask for memory, MM(FastMM) asks the OS for a large chunks and then it splits them and gives you a piece (based on the size you need). When the object is destroyed (free), the memory is returned to the MM. Now, based on the returned size, the MM may either choose to recycle the object location (if its small) or return the memory to the OS (a real-free-op).

What you're doing in MultiplyStr is not just wasteful but extremely harmful ! For each iteration you're reallocating memory. Allocating a new block and copying the old block to the new one. It's very important to know that small block are implemented as a segregated list. i.e if you ask for a 32 bytes, MM on reality allocates an entire table i.e 32x32=1024 bytes and yields first block. In your example you said you used 1GB ! this is extremely bad because you're not economizing resources and you'll quickly run out of memory i.e another thread that asks for a large chunk.

It's indeed a good practice to pre-allocate memory : 


function MultiplyStr(aStr: string; aMultiplier: integer): string;
var i: Integer;
begin
  SetLength(Result, length(aStr) * aMultiplier);
  for i := 1 to aMultiplier do
    Result := Result + aStr;
end;

 Please run the above and notice the memory and performance !!!

Also a small remark ! aStr should be const !!!

 

Actually, your code is wrong.

Now you are doubling the memory usage.

SetLength creates a big string, then in the loop, you add more to it...

 

If you wish to pre-allocate the length of the string, then you need to do it this way:

 

  Function MultiplyStrWithPreallocate(aStr: String; aMultiplier: Integer): String;

  Var i, pos: Integer;

  Begin

    SetLength(Result, Length(aStr) * aMultiplier);

    pos := 1;

    For i := 1 To aMultiplier Do

    Begin

      move(aStr[1], Result[pos], Length(aStr) * sizeOf(Char));

      Inc(pos, Length(aStr));

    End;

  End;

 

Here is what is going on behind the scene, when you call:

Result := Result + aStr;

the memory manager creates a new temporary string, assigns the content of "result" and "aStr" to it. That new temporary Strings is then assigned to "result" which in turn decreases the reference counter to the String that result is refering... in that case to 0, which causes the memory manager to release that String.

So for a short period of time, both, the original "result" and the "result"+"aStr" are in memory.

And for each for-loop-iteration, you basically have a copy operation of the whole string.

 

My above optimization using move reduces both of those concerns. There is just one memory allocation, and only a few bytes are copied in each for-loop-iteration

  • Like 1
  • Thanks 1

Share this post


Link to post
37 minutes ago, Fons N said:

There is a short explanation to the FastMM internals in  Mastering Delphi Programming: A Complete Reference Guide (https://www.packtpub.com/product/mastering-delphi-programming-a-complete-reference-guide/9781838989118). Along many other good stuff. The PDF version is very cheap. Really worthwhile,

 

 

Thank you for suggesting exactly what gives the relevant info, and not the whole book! 😉 

Share this post


Link to post
20 hours ago, Mike Torrettinni said:

Interesting! I just ran the same example in D2006 and result is almost the same, at the end of the parsing the memory comes down to almost as it was at the start. Peak memory consumption is very similar, around 1.1GB.

I don't have D7 installed to try.

Of course, because FastMM was integrated in D2006 already, so results are pretty much similar 😉 I thought it was in D2007.

Edited by Mike Torrettinni

Share this post


Link to post
3 minutes ago, Mike Torrettinni said:

Thank you for suggesting exactly what gives the relevant info, and not the whole book! 😉 

I am not sure how to read this... is it suppose to be sarcastic?  I cannot just copy paste the relevant pages of the book - that would be illegal. And for just 5 euros the PDF version is a steal and again really worthwhile,

 

Share this post


Link to post
8 minutes ago, Fons N said:

I am not sure how to read this... is it suppose to be sarcastic?  I cannot just copy paste the relevant pages of the book - that would be illegal. And for just 5 euros the PDF version is a steal and again really worthwhile,

 

No, no, it's refreshing to get a suggestion to read only what is relevant to the topic. I actually just read it!  Usually I get suggestions to read whole books just for a simple question 🙂

Edited by Mike Torrettinni

Share this post


Link to post

Wow, even with Delphi 7 the results are the same! And it doesn't have FastMM at all!

I don't have earlier versions to test, but I assume it's pretty much the same: Delphi cleans up the memory usage pretty good, even if you use really bad code with strings. As long as it doesn't crash (out of memory) string manipulation is pretty much safe and memory manager pretty much cleans up and releases memory back to the system.

Share this post


Link to post
Just now, David Heffernan said:

Are you measuring address space fragmentation? 

No, but doesn't windows handle the released memory quite efficiently? The memory consumption from just this test is 10GB+ and system has no less available memory after the test is done.

  • Haha 1

Share this post


Link to post
Guest

hi @Mike Torrettinni

 

As you like to improve your knowledge and discover the world of bits, I don't know if you know "ILSC".
It allows you to monitor and release "Standby" memory allocated by the software, which, at great cost, is returned to the operating system.
This can be seen better during games, or software that makes use of large amounts of memory for rederization, for example.
Maybe it will help you with your work.

 

image.thumb.png.4e366c8243d27574cee43b874482525c.png   image.thumb.png.4d7e606b5f9e8875af05a663d990a4d3.png

 

https://www.wagnardsoft.com/forums/viewtopic.php?f=18&t=1256

Intelligent standby list cleaner by WagnardSoft (<600KB)

The application will monitor and clear the memory standby list according to the configured options parameter you set.

It may help users who have stutters in games when using windows 10 Creator update and higher version of windows.

Quote

Unfortunately, I am not updating this micro-software anymore ... or at least, no update is needed at this time.

 

Edited by Guest

Share this post


Link to post
1 hour ago, emailx45 said:

t allows you to monitor and release "Standby" memory allocated by the software, which, at great cost, is returned to the operating system.

Is this Cached memory? If yes, I think Windows 10 handles this very well on its own,

 

Here is my memory on start:

image.png.46d5eaea6ee46ab773fb0615e2ce665e.png

 

 

and after 10x(!) running this wasteful example - i didn't measure exactly, but each run uses and releases 10GB+ of memory, so 10x runs used and released 100 GB+ memory:

 

image.png.3bf35ebff29e575709ba5a754ef60d15.png

 

Cached went up by only 1GB (while my system running and not sure if other apps didn't contribute some cache also).

 

I guess the conclusion still stands: Delphi is very good with managing and releasing memory, even if the code is absolutely bad in terms of string handling.

Edited by Mike Torrettinni

Share this post


Link to post
20 minutes ago, David Heffernan said:

If you don't know about address space fragmentation, don't you wonder whether or not it is relevant? 

The feedback on this topic has been very clear and I like the conclusion that even non-perfect code will not break the system memory or cause it to run out of it. Like with memory leaks, most of the time when you exit the process, the memory gets released back to the system.

I understand the OS is not perfect in memory handling, but if it handled such extreme example, it will also handle anything I throw at it in production code.

 

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  

×