General Qestions about compiler interpretations

Blitz3D Forums/Blitz3D Beginners Area/General Qestions about compiler interpretations

_PJ_(Posted 2013) [#1]
Okay, so I am never sure when it comes to certain functionality just how they might be compiled in Blitz, and therefore what's the "best" way to approach them:

Take for example:
For n=1 To CountGfxDrivers()

Will this mean that a call to CountGfxDrivers will be made every time the For/Next loop runs, in order to check whether to stop, or is it actually storing the result in a register and only checking the register?
Which would mean that
Local GfxDrivers=CountGfxDrivers()
For n=1 to GfxDivers
is not necessary.

What about Types.
If I am performing various functions on a Type Instance, according to the debug-mode window, every time a function is called with a Type Instance parameter, that type instance is repeated in the local scope of the function.
Are there any real preferences in terms of pure memory use?
Type MYTYPE
 Field LotsOfFieldsAndData[6352]
 Field LotsMoreFieldsAndData[76329]
 Field ABankHandleFullOfData
End Type

Function DoStuffToTypes()
 Local Iterator.MYTYPE
 For Iterator = Each MYTYPE
  DoOneThing(Iterator)
  DoAnotherThing(Iterator)
 Next


As Opposed to:
Function DoStuffToTypes()
 Local Iterator.MYTYPE
 Local Current
 For Iterator = Each MYTYPE 
  Current=Handle(Iterator)
  DoOneThingToHandle(Current)
  DoAnotherThingHandle(Current)
 Next



I know there was something else, but it eludes me at ther moment...


Yasha(Posted 2013) [#2]
Will this mean that a call to CountGfxDrivers will be made every time the For/Next loop runs, in order to check whether to stop


Yes. Note that this behaviour was changed for BlitzMax (to the cached-version you suggest above), because a lot of people, Mark himself included, were annoyed at the "clearer" way to write code also being deceptively inefficient.

This will make absolutely no difference to performance unless the loop is an inner one or the function is very slow indeed.


If I am performing various functions on a Type Instance, according to the debug-mode window, every time a function is called with a Type Instance parameter, that type instance is repeated in the local scope of the function.
Are there any real preferences in terms of pure memory use?


...?

Type instances are allocated on the heap. The variables on the stack contain a four-byte pointer to the instance's location on the heap. If you assign an instance from one variable to another variable, there are now two variables containing this same four-byte pointer to the same area of heap memory; the contents of the heap memory are not copied. The only difference having more references to the same instance makes memory-wise is that a function's call frame will be four bytes larger for each variable (which is true for every kind of variable in B3D because ints, floats, and string pointers are all the same size). As with integers and so on, the amount of space used on the stack is constant since the number of variables, regardless of what they hold, does not change.

This is so negligible as to not be worth measuring and will have no observable effect on code that doesn't have a stack overflow or similar.

They're pretty much identical and it only really depends on whether you have a particular reason to use Handles or not (note that Handles are sometimes slower as a general replacement for pointers, as they take a measurable amount of time to convert).

--------+--------------------+---------------+--------
 (prev) |  DoStuffToTypes    |  DoOneThing   | (next) 
  ...   | Iterator.MYTYPE[4] | Arg.MYTYPE[4] |  ...               |           ...           |
             |                  |                                 +-------------------------+
             |------------------|-------------------------------> | Instance0(MYTYPE)[HUGE] |
                                                                  +-------------------------+
                                                                  |           ...           |




--------+------------------------------------+---------------------------+--------
 (prev) |  DoStuffToTypes                    |  DoOneThingToHandle       | (next) 
  ...   | Current.Int[4], Iterator.MYTYPE[4] | Arg.Int[4], Val.MYTYPE[4] |  ...   
                             |                              |                     
                             |------------------------------|                     
                                                            |
                                                            |
                                                            V
                                               |           ...           |
                                               +-------------------------+
                                               | Instance1(MYTYPE)[HUGE] |
                                               +-------------------------+
                                               |           ...           |



There are however two neat tricks for taking advantage of byref semantics to either reduce memory usage, or to make things slightly easier (you can use either objects or arrays for these, but I prefer arrays as it makes the intent clearer and they also autocreate/delete themselves):

1) Multiple return values from a function:

Function GetEntityCoords(ent, out#[2])
    out[0] = EntityX(ent) : out[1] = EntityY(ent) : out[2] = EntityZ(ent)
End Function

;To call:
Local coords#[2]
GetEntityCoords myEntity, coords


When you create a "local array", it is equivalent to doing CreateBank at the start of the function and FreeBank at the end of the function. You can't return them, but you can pass them "upward" into other calls, and because they exist throughout the inner call's lifespan, they can be used to retrieve data from it, as the array is passed into the inner call "by reference" and therefore the same array is mutated from a different position.

2) Speeding up string handling:

Function MyDeeplyNestedStringFunc(s$)
    Local s_$[0] : s_[0] = s
    MyDeeplyNestedStringFunc1 s_
End Function

Function MyDeeplyNestedStringFunc1(s$[0])
    ...
    MyDeeplyNestedStringFunc2 s
End Function

Function MyDeeplyNestedStringFunc2(s$[0])
    ...
    MyDeeplyNestedStringFunc3 s
End Function

Function MyDeeplyNestedStringFunc3(s$[0])
    ...
    MyDeeplyNestedStringFunc4 s
End Function

...


Strings actually do copy (at least some) data on every assignment, including the implicit assignment to any function's parameter slot (at the very minimum, a new 24-byte header object gets created; it's not crushingly slow but it is several times slower than plain numbers). If you pass strings up and down a lot but don't actually access them that often, you can stick the string in an array once at the base of your call chain, and each parameter pass will simply copy the four-byte array pointer, which is as fast as passing an integer. When you actually need to get at the string value (e.g. to pass to a builtin function), dereferencing with s[0] is almost free because it's a constant offset (this has a dedicated machine instruction). This can speed up a string-heavy program significantly, by only copying the string on assignments where you can't avoid it.


On the subject of registers -

B3D will never but never use a register as a variable slot. Every variable goes on the stack. Therefore, some "optimisations" that cache values in variables so they don't get recomputed can actually slow down really minimal functions, by taking the recomputed value out of registers and forcing it to be written to and then read from the stack.

That said, if you care about this level of optimisation you should not be using B3D anyway; it's near impossible to tell what effect any change will have without testing it and there is no way to get at low-level efficiencies (for instance, you can double/halve the execution time of a function just by reordering the statements because the compiler isn't aware of instruction alignment and will just blindly output them compacted and in order). On the other hand, on modern systems cache is so fast that you don't really need this stuff anyway.


Floyd(Posted 2013) [#3]
The second question, involving handles, is mysterious.

The first is easily answered by testing the behavior.

For n = 1 To IteratorTest()
	Print n
Next

WaitKey

Function IteratorTest()
	Print "IteratorTest() has been called."
	Return 5
End Function



_PJ_(Posted 2013) [#4]
Thanks for the great responses.

@Yasha I know Im pretty 'paranoid' sometimes and overzealous with my optimisation stuff, but in this case, it was really just curiosity to be honest!

The ability to create local arrays is brilliant! Thanks for sharing & enlightenting.



@Floyd,
I never thought of that :) Thank you!