Jump to content

Stack and functions.


steph Arnott
 Share

You are about to reply to a thread that has been inactive for 3840 days.

Please take a moment to consider if this thread is worth bumping.

Recommended Posts

I confused with this, In a recent post, I was told that I should not use functions unless called more than once (somthing to do with memory having to be allocated). But as I researched this I have learned that the stack increases when the function is called and reduces when the code block ends.

"The byte-code memory is where your script lives while running. 

The heap holds strings, lists, and keys.

The stack holds everything else; it grows each time you enter a new block of code (such as a function),
and shrinks every time you exit a block."

So I can not see what gain there is in not havening a code block as a function even if it called only a few times.

Sassy Romano

" For example when to inline and when to function.  A function always starts on a new 512byte boundary thus for every function you define, that's another minimum of 512 bytes gone even if it's just a function to add two numbers."

Link to comment
Share on other sites

Hi Steph,

Imagine you have a Sassy's function to add two numbers... add(n1,n2):

float add(float n1, float n2){
    return(n1+n2);
}

And you'd call it with:

n3=add(n1,n2);

As Sassy said, our trivial "add" function will compile on a new 512 byte boundary, so the smallest it can be is 512 bytes.

But, if you coded this "add" inline, it would look like:

n3=n1+n2;

And probably compile into just one byte.

In addition to the 512 byte minimum size of a function, the compiler must generate byte-codes in your program to put the function's arguments on the stack, put the return address (the location of the statement after the function call) there too, and finally retrieve the result from the stack. That overhead will be at least a few bytes of byte-code, so it's quite possible that the byte-code cost for making a function call (even ignoring the space taken by the function itself) can be greater than the inline cost of a trivial function.

In a trivial case such as this, turning "add" into a function would make no sense at all. You waste 512 bytes by creating the function and another few bytes each time you call it. But there is a point at which it makes sense to turn something into a function. I can't tell you exactly where that point is, it depends on the amount of byte-code in the function, the function call overhead (which will depend on the number and kind of arguments passed to it and returned from it) and how many times you call it, and we really have no easy way of measuring a function's size, nor the overhead of calling one.

But let's say we did, and we discover this for another pretend function called foo()...

Function call overhead = 4 bytes.
Size of foo's code when compiled inline = 191 bytes

So, we know that turning foo() into a function will increase its size from 191 to 512 bytes, and that every time we call it, we'll consume another 4 bytes of script memory. It then becomes possible to determine at what point it's worth turning foo into a function.

First, there's no reason to turn anything into a function if you're only going to call it once, so we'll start by looking at two calls.

Inline cost
    2 x 191 bytes = 382 bytes

Function cost
    2 x call overhead (4 bytes) = 8
    1 x foo as a function = 512 bytes
    Total = 520 bytes

Function cost (520) > Inline Cost (382)

So it makes no sense to turn foo into a function if you'll call it only twice.

Now let's look at calling foo three times...

Inline cost
    2 x 191 bytes = 573 bytes

Function cost
    3 x call overhead (4 bytes) = 12
    1 x foo as a function = 512 bytes
    Total = 524 bytes

Function cost (524) < Inline cost (573)

Now, it does make sense turn foo into a function.

Calling a function takes longer than executing it inline, though in the case of LSL, that overhead may be negligible, particularly if the function does things that take a lot of time.

As Sassy stated, calling a function also takes some stack space, as the script must pass the numbers to the function, remember where to return after calling the function (the statement after the call), and return the result. All of that (for simple functions) is done on the stack. Heap usage is probably the same whether you inline or use a function.

 

 

  • Like 1
Link to comment
Share on other sites

"Tests in January 2013 show that user functions in Mono no longer (if ever) automatically take up a block of memory (512 bytes) each. As with all Mono code, an extra 512-block of memory is used when needed, but not automatically per user function. In addition the perceived memory usage can vary depending, it is thought, on the action of periodic garbage collection. Inserting an llSleep() for instance, maybe allowing garbage collection to jump in, before reading memory usage, can show a reduction in space used."

Link to comment
Share on other sites


steph Arnott wrote:

"Tests in January 2013 show that user functions in Mono no longer (if ever) automatically take up a block of memory (512 bytes) each. As with all Mono code, an extra 512-block of memory is used when needed, but not automatically per user function. In addition the perceived memory usage can vary depending, it is thought, on the action of periodic garbage collection. Inserting an llSleep() for instance, maybe allowing garbage collection to jump in, before reading memory usage, can show a reduction in space used."

If that's the case, then the advantage of using functions will come after fewer uses of the code, so long as the function is larger than the calling overhead. Again, we don't know how big a function is, nor how big the calling overhead is, so we can't be certain whether turning something into a function makes sense. I was not aware of the 512 byte code block size until reading Sassy's description.

In my professional life, I have tools that tell me exactly what the cost of everything is, both in terms of memory usage and execution time, so it's always possible and often easy to determine whether to code something inline or as a function. Absent such tools, I'd code for ease of understanding, and that would often mean turning something into a function the moment doing so makes my program easier to figure out. And that may mean (horror of horrors) turning something into a function that gets called only once, but is used so often in my programming that it's nice to have it all wrapped up with a pretty bow.

And this practice is commonplace in the programming world, resulting in collections of functions called "libraries". Depending on the sophistication of the programming environment, including a library in your project may include all the functions therein, even if you don't call them, or may bring in only those functions you do call. As always, good programming is a balancing act between the constraints of the systems the programs run on, and the constraints of the organizations that hire the programmers and purchase the programming tools to get the job done.

Link to comment
Share on other sites

It's NOT just the numbers. My time is worth much more :)

If I change a script a few months after I have written it - do I want to see the same inline code at multiple places, making it hard to read? - nope (I have no problems with one liners though)

If I have a function that I will eventually change - do I want to search all the inliners for that? - nope - I will do that change at one place - in the function.

If I have a block of 100 lines inside of a if/then construct I will probably make a function so I understand the code in one view If I have a look at it a few months later.

 

Saving bytecode is fine and I do it, but I have other priorities than that.

Link to comment
Share on other sites

In computer programming, there is a thing known as Rule of Threes (http://en.wikipedia.org/wiki/Rule_of_three_(computer_programming)). Basically what it means is that the third occurrence of anything in your code should be considered for refactoring. In other words, calling llDetectedOwner() is required if done once, maybe done twice, but if it's done three or more times-> it's much better to declare a variable for it, assign the return of llDetectedOwner() to it and access the variable that from then on. This works because accessing the value of a variable is way better than calling a library function and you offset both the memory requirement of the added variable and the time needed to initialize it with just one call to the library function.

 

Same works with inline code blocks. If you have three or more of them that are functionally the same, they should all be consolidated and made into a subroutine, Whatever overhead might be incurred is more than offset by the reduced lines in your code and the increased maintainability. A listener channel should be a variable simply because you only need to change the value of that variable once as opposed to finding every instance of that channel being used and changing them all. By the same token, having only one subroutine that is called from 3 or more places lessens your code body, decreases you chance of error  and simply makes a more coherent script by keeping like logical constructs together.

 

OK, so that gives an idea of when to have a subroutine but your question is about when not have one. And that's easy if you actually think about it: If a subroutine is called from 3 or more locations within your code, it should remain- Benefits of it will outweigh any overhead involved. If it's called twice, take it on a case by case basis, sometimes a subroutine is better. But if it's only called once, it should be inlined. And I say this because you not only incur stack overhead (whatever it might be!) but you have split apart your code unnecessarily, making it more difficult to maintain.

  • Like 1
Link to comment
Share on other sites

The main thing to remember is that even though LSL has C syntax it is not C. There is no concept of pointer in LSL so arguments are passed to functions not by reference but by value which means it puts a whole object on the stack instead of the object's address. If for example your program has a list of let's say 10K size and you pass it as an argument to a function you'll end up with a 10K list in a global namespace PLUS a 10K list on the stack so you must have 20K available. for this reason whether or not to have a user function depends entirely on the available memory. If a program takes let's say 40K memory putting on the stack a 10K list would not cause much problem. However if your program is 55K of memory, you might have a stack-heap collision when passing a 10K list as an argument to a user function.

Link to comment
Share on other sites


Ela Talaj wrote:

Would be kinda useful if LSL preprocessor understood
inline
keyword but I guess it is too much to ask from the poor thing
:)

 

What is really useful is remembering that even though LSL has C syntax it is not C. So, when one sees the word inline or in-line in a discussion of LSL they'd know it could not possibly be referring to a keyword but must, necessarily, be used as a mere adjective, as is done with CSS for example.

Link to comment
Share on other sites


steph Arnott wrote:

"Tests in January 2013 show that user functions in Mono no longer (if ever) automatically take up a block of memory (512 bytes) each."

Thanks for that, I didn't know that had changed and the last time i'd gone through a script trying to eek out all available memory was before Jan 2013 :matte-motes-sunglasses-1:

Link to comment
Share on other sites

You are about to reply to a thread that has been inactive for 3840 days.

Please take a moment to consider if this thread is worth bumping.

Please sign in to comment

You will be able to leave a comment after signing in



Sign In Now
 Share

×
×
  • Create New...