26 November 2020

The Naked attribute in practice

In the previous posts The Anatomy of a Procedure (1) and The Anatomy of a Procedure (2) I discussed the effect of the Naked attribute on the code generated by the compiler. Everything you want to know about the Naked attribute can be found in these posts, but – unfortunately – the posts are rather technical. If you lack any experience in assembly it might be hard to understand, so I will recap on the use of the Naked attribute in ‘layman’s’ terms here.

Naked explained
A Naked procedure is fully optimized, both in size and in performance. This comes with a penalty though, a naked procedure lacks support for dynamic variables types (String, Object, Variant, array and hash), structured exception handling, and runtime debugging (Tron, Trace). The reason for this is the lack of ‘procedure-housekeeping’ that GFA-BASIC 32 inserts in each regular procedure. In a regular procedure GB starts of by inserting a 80 bytes stackframe (68 in an EXE) to store all information necessary for housekeeping of the procedure. At the end of the procedure in insert code to restore the stack to automatically release all (dynamic) variables (even in case of a runtime error). The housekeeping code is missing in a naked procedure. Consequently, a naked procedure can execute faster, in certain cases up to 50% faster than a regular procedure. Only short procedures benefit from the Naked attribute; the executable code must be relative small compared to the code necessary to setup a stackframe of 80 bytes, as we will see. The example procedure from the previous post is a good example of a candidate for Naked, it executes 50% faster:

TestMul(2, 3)
Proc TestMul2(x As Int, y As Int) Naked
  Local tmp As Int
  tmp = x Mul y

When the procedure grows and contains more executable code the relevance of Naked disappears. The next example shows two things. First, it declares a local dynamic string variable (which needs to be released explicitly by setting it to the ‘empty-string’). Secondly, the assignment of data to the string will allocate memory and produce code to copy the data to that memory. This is a relatively expensive operation and will reduce the possible performance gain of Naked.

Dim i%, t#
t# = Timer
For i% = 0 To 100000 : TestMul(2, 3) : Next
Debug "Normal: "; Timer - t#
t# = Timer
For i% = 0 To 100000 : TestMul2(2, 3) : Next
Debug "Naked: "; Timer - t#

Proc TestMul(x As Int, y As Int)
  Local tmp As Int, s As String = "Something"
  tmp = x Mul y
Proc TestMul2(x As Int, y As Int) Naked
  Local tmp As Int, s As String = "Something"
  tmp = x Mul y
  s = ""

The measured time for calling TestMul() and TestMul2() a 10000 times is

TestMul: ca 0.021 seconds
TestMul2: ca 0.019 seconds.

In this example, the time to execute the naked procedure is almost the same as executing the regular procedure. Adding a dynamic variable – and assigning it a value - to a naked procedure negates the benefit almost entirely. The code to execute is drastically increased, assigning a value to a string causes the execution of malloc() and memcopy(), and these take so much time the advantage of naked is almost gone. In addition, the string must be released which leads to an extra call of mfree(). All in all, just by adding one dynamic variable the procedure contains too much code to really benefit from Naked.

Other issues
Another disadvantage of using Naked is the issue of runtime error trapping. In case of an error the IDE stops running the program and puts the the error-line-marker on the line that calls the procedure and not inside the naked procedure.

A non-naked, regular procedure will not only trap the error, but also clears the contents of the (dynamic) local variables automatically. With naked-procedures the dynamic variables must be released explicitly. The local variables can be released using the following commands.

Local dynamic variable        Release command           
String s$ = “” or Clr s$
Variant var = Empty
Object (any COM) Set obj = Nothing
Hash Hash Erase hs[]
Array (not allowed, unless static)   If static: Erase ar()

A local array cannot be used in a naked-procedure. A local array declaration allocates an array-descriptor plus the memory required to store the array elements. Using the Erase command on a local array only releases the memory for the data, not the array-descriptor. Each time the procedure is executed a new descriptor is allocated without being released. Consequently, the program will leak memory. If you need a local array it must be static, then the descriptor is allocated only once. This is not a problem in regular procedures, of course.

Only short procedures that don’t use local dynamic variables are candidates for the Naked attribute.