Menu

#1013 Non-optimization bug for 'New[]' / 'Delete[]' when datatype has no 'Constructor' (and without '{Any}' initializer)

open
nobody
None
compiler
2025-10-08
2025-07-29
No

For more information, see discussion at:
https://www.freebasic.net/forum/viewtopic.php?t=33256

Without 'Any' initializer, 'Redim' / 'Erase' on numeric datatype induces a run time similar to the one with 'Callocate' / 'Deallocate', while 'New[]' / 'Delete[]' induces a run time 1000 times greater for the example below.
It is why I consider that it is a non-optimization bug for 'New[]'/'Delete[]' when datatype has no 'Constructor'/'Destructor' (and without '{Any}' initializer).

Example (compile without code optimization option):

Dim As Double t

Print "MEMORY (LONGS) NOT INITIALIZED ON SIZING"
Print "----------------------------------------"
Print

Print "Memory sizing (sizing then freeing) using 'Allocate'/'Deallocate':"
t = Timer
For I As Integer = 1 To 10
    Dim As Long Ptr p = Allocate(10000000 * Sizeof(Long))
    Deallocate(p)
Next I
t = Timer - t
Print " total (sizing + freeing):"; t; " s."
Print

Print "Memory sizing (sizing then freeing) using 'REDIM+=ANY'/'ERASE ':"
t = Timer
For I As Integer = 1 To 10
    Redim As Long a(10000000 - 1) = Any
    Erase a
Next I
t = Timer - t
Print " total (sizing + freeing):"; t; " s."
Print

Print "Memory sizing (sizing then freeing) using 'New[]+{ANY}'/'DELETE[]':"
t = Timer
For I As Integer = 1 To 10
    Dim As Long Ptr p = New Long[10000000] {Any}
    Delete[](p)
Next I
t = Timer - t
Print " total (sizing + freeing):"; t; " s."
Print

Print "============================================================================="
Print

Print "MEMORY (LONGS) INITIALIZED TO ZERO ON SIZING"
Print "--------------------------------------------"
Print

Print "Memory sizing (sizing then freeing) using 'Callocate'/'Deallocate':"
t = Timer
For I As Integer = 1 To 10
    Dim As Long Ptr p = Callocate(10000000 * Sizeof(Long))
    Deallocate(p)
Next I
t = Timer - t
Print " total (sizing + freeing):"; t; " s."
Print

Print "Memory sizing (sizing then freeing) using 'REDIM'/'ERASE ':"
t = Timer
For I As Integer = 1 To 10
    Redim As Long a(10000000 - 1)
    Erase a
Next I
t = Timer - t
Print " total (sizing + freeing):"; t; " s."
Print

Print "Memory sizing (sizing then freeing) using 'New[]'/'DELETE[]':"
t = Timer
For I As Integer = 1 To 10
    Dim As Long Ptr p = New Long[10000000]
    Delete[](p)
Next I
t = Timer - t
Print " total (sizing + freeing):"; t; " s."
Print

Sleep

Output example:

MEMORY (LONGS) NOT INITIALIZED ON SIZING
----------------------------------------

Memory sizing (sizing then freeing) using 'Allocate'/'Deallocate':
 total (sizing + freeing): 0.0001793000041914183 s.  <== OK

Memory sizing (sizing then freeing) using 'REDIM+=ANY'/'ERASE ':
 total (sizing + freeing): 0.0001632000008129353 s.  <== OK

Memory sizing (sizing then freeing) using 'New[]+{ANY}'/'DELETE[]':
 total (sizing + freeing): 0.000166699998615627 s.  <== OK

=============================================================================

MEMORY (LONGS) INITIALIZED TO ZERO ON SIZING
--------------------------------------------

Memory sizing (sizing then freeing) using 'Callocate'/'Deallocate':
 total (sizing + freeing): 0.0001539999972806072 s.  <== OK

Memory sizing (sizing then freeing) using 'REDIM'/'ERASE ':
 total (sizing + freeing): 0.0001635999952540601 s.  <== OK

Memory sizing (sizing then freeing) using 'New[]'/'DELETE[]':
 total (sizing + freeing): 0.1539328999944942 s.  <== NOK : Non-optimization bug

I compared 'Redim' / 'Erase' with 'New[]' / 'Delete[]' on UDT with or without 'Constructor' / 'Destructor'.
(3 '#define' to activate/deactivate 'Constructor' / 'Destructor' / 'Any' initializer)
Example (compile without code optimization option):

#define withCtor
#define withDtor
#define withAny

#ifdef withCtor
Print "UDT HAS A DEFAULT 'CONSTRUCTOR'"
#else
Print "UDT HAS NO DEFAULT 'CONSTRUCTOR'"
#endif
#ifdef withDtor
Print "UDT HAS A 'DESTRUCTOR'"
#else
Print "UDT HAS NO 'DESTRUCTOR'"
#endif
#ifdef withAny
Print "MEMORY ALLOCATED WITH 'ANY' INITIALIZER"
#else
Print "MEMORY ALLOCATED WITHOUT 'ANY' INITIALIZER"
#endif
Print

Dim As Double t1
Dim As Double t2

Type UDT
    Dim As Byte b
    #ifdef withCtor
    Declare Constructor()
    #endif
    #ifdef withDtor
    Declare Destructor()
    #endif
End Type
#ifdef withCtor
Constructor UDT()
End Constructor
#endif
#ifdef withDtor
Destructor UDT()
End Destructor
#endif

#ifdef withAny
Print "Memory sizing (sizing then freeing) using 'REDIM+=ANY'/'ERASE ':"
#else
Print "Memory sizing (sizing then freeing) using 'REDIM'/'ERASE ':"
#endif
t1 = Timer
#ifdef withAny
Redim As UDT u(100000000 - 1) = Any
#else
Redim As UDT u(100000000 - 1)
#endif
t1 = Timer - t1
Print " sizing with Redim:"; t1; " s."
t2 = Timer
Erase u
t2 = Timer - t2
Print " freeing with Erase:"; t2; " s."
Print " total (sizing + freeing):"; t1 + t2; " s."
Print

#ifdef withAny
Print "Memory sizing (sizing then freeing) using 'NEW[]+{ANY}'/'DELETE[]':"
#else
Print "Memory sizing (sizing then freeing) using 'NEW[]'/'DELETE[]':"
#endif
t1 = Timer
#ifdef withAny
Dim As UDT Ptr pu = New UDT[100000000] {Any}
#else
Dim As UDT Ptr pu = New UDT[100000000]
#endif
t1 = Timer - t1
Print " sizing with New[]:"; t1; " s."
t2 = Timer
Delete[](pu)
t2 = Timer - t2
Print " freeing with Delete[]:"; t2; " s."
Print " total (sizing + freeing):"; t1 + t2; " s."
Print

Sleep

Output example for 'Constructor' and 'Destructor' but no 'Any' initializer:

UDT HAS A DEFAULT 'CONSTRUCTOR'
UDT HAS A 'DESTRUCTOR'
MEMORY ALLOCATED WITHOUT 'ANY' INITIALIZER

Memory sizing (sizing then freeing) using 'REDIM'/'ERASE ':
 sizing with Redim: 1.646054299994916 s.  <== OK
 freeing with Erase: 1.406641900001688 s.  <== OK
 total (sizing + freeing): 3.052696199996603 s.  <== OK

Memory sizing (sizing then freeing) using 'NEW[]'/'DELETE[]':
 sizing with New[]: 1.677848100002819 s.  <== OK
 freeing with Delete[]: 1.43740989999359 s.  <== OK
 total (sizing + freeing): 3.115257999996409 s.  <== OK

Output example for no 'Constructor' and no 'Destructor' and no 'Any' initializer:

UDT HAS NO DEFAULT 'CONSTRUCTOR'
UDT HAS NO 'DESTRUCTOR'
MEMORY ALLOCATED WITHOUT 'ANY' INITIALIZER

Memory sizing (sizing then freeing) using 'REDIM'/'ERASE ':
 sizing with Redim: 1.680000217874067e-005 s.  <== OK
 freeing with Erase: 1.359999350825092e-005 s.  <== OK
 total (sizing + freeing): 3.039999568699159e-005 s.  <== OK

Memory sizing (sizing then freeing) using 'NEW[]'/'DELETE[]':
 sizing with New[]: 0.02655439999644926 s.  <== NOK : Non-optimization bug
 freeing with Delete[]: 0.008183699993196569 s.  <== nok
 total (sizing + freeing): 0.03473809998964583 s.  <== NOK : Non-optimization bug

Output example for no 'Constructor' and no 'Destructor' but 'Any' initializer:

UDT HAS NO DEFAULT 'CONSTRUCTOR'
UDT HAS NO 'DESTRUCTOR'
MEMORY ALLOCATED WITH 'ANY' INITIALIZER

Memory sizing (sizing then freeing) using 'REDIM+=ANY'/'ERASE ':
 sizing with Redim: 1.819999672392214e-005 s.  <== OK
 freeing with Erase: 1.589999659046271e-005 s.  <== OK
 total (sizing + freeing): 3.409999331438485e-005 s.  <== OK

Memory sizing (sizing then freeing) using 'NEW[]+{ANY}'/'DELETE[]':
 sizing with New[]: 2.109999961419362e-005 s.  <== OK
 freeing with Delete[]: 1.429999475277555e-005 s.  <== OK
 total (sizing + freeing): 3.539999436696917e-005 s.  <== OK

**To fix the problem of non-optimization in case of no 'Constuctor' and no 'Destructor' and no '{Any}' initializer:

  • compare the optimization kinds between 'Redim'/'Erase' and 'New[]'/'Delete[]',
  • for 'New[]', may be 'Callocate' is not called but wrongly 'Allocate' + 'Clear' (the difference in run time would correspond),
  • for 'Delete[]', may be a consequence of the 'Allocate' + 'Clear' above, on deallocation time (the difference in run time would correspond).**

After SARG code inspection, it is confirmed that New / New[] starts by calling:

  • 'malloc' if the 'Any' initializer is used (OK),
  • otherwise 'malloc' + 'memset(0)' (NOK).
    On the other hand, Dim / Redim starts by calling:
  • 'malloc' if the 'Any' initializer is used (OK),
  • otherwise 'calloc' if no fix-len string is used (OK),
  • otherwise 'malloc' + 'memset(space)' if the fix-len string is used (OK).

For large buffers (upwards of several hundred KB), the memory coming from the operating system is already zeroed.
Test code:

Function isMemoryZeroedFromAllocate(Byval size As Uinteger) As Boolean
    Dim As Byte Ptr p = Allocate(size)
    For I As Uinteger = 0 To size - 1
        If p[I] <> 0 Then Deallocate(p) : Return False
    Next I
    Deallocate(p)
    Return True
End Function

Print "'X': memory not zeroed from 'Allocate' (by step of 1 KB allocated)"
Print "'0': memory zeroed from 'Allocate' (by step of 1 KB allocated)"
Print
For I As Integer = 1 * 1024 To 2048 * 1024 Step 1 * 1024
    Dim As Boolean b = isMemoryZeroedFromAllocate(I)
    If b = True Then Print "0"; Else Print "X";
Next I
Print

Sleep

Output on my PC Windows for 64-bit code:

'X': memory not zeroed from 'Allocate' (by step of 1 KB allocated)
'0': memory zeroed from 'Allocate' (by step of 1 KB allocated)

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000

For Windows 64-bit code, memory zeroed from about 1000 KB allocated.
(for Windows 32-bit code, I find a memory zeroed from about 500 KB allocated)

Because 'calloc' is integrated into the memory allocator, it knows if the memory returned comes from the operating system and, if so, it skips the call to 'memset(0)'.
So for such buffers, 'calloc' is much faster than 'malloc' + 'memset(0)'.

Comparison of 1,000,000 zero-initialized 1KB memory allocations, with 10 zero-initialized 100MB memory allocations:

  • for 'Allocate' + 'Clear' / 'Deallocate'
  • versus 'Callocate' / 'Deallocate'
    Example (compile without code optimization option):
Dim As Double t

Print "1,000,000 zero-initialized 1KB memory allocations:"

Print "   Memory sizing (sizing then freeing) using 'Allocate'+'Clear'/'Deallocate':"
t = Timer
For I As Integer = 1 To 1000000
    Dim As Byte Ptr p = Allocate(1024)
    Clear p[0], 0, 1024
    Deallocate(p)
Next I
t = Timer - t
Print "      total (sizing + freeing):"; t; " s."

Print "   Memory sizing (sizing then freeing) using 'Callocate'/'Deallocate':"
t = Timer
For I As Integer = 1 To 1000000
    Dim As Byte Ptr p = Callocate(1024)
    Deallocate(p)
Next I
t = Timer - t
Print "      total (sizing + freeing):"; t; " s."
Print

Print "10 zero-initialized 100MB memory allocations"

Print "   Memory sizing (sizing then freeing) using 'Allocate'+'Clear'/'Deallocate':"
t = Timer
For I As Integer = 1 To 10
    Dim As Byte Ptr p = Allocate(100 * 1024 * 1024)
    Clear p[0], 0, 100 * 1024 * 1024
    Deallocate(p)
Next I
t = Timer - t
Print "      total (sizing + freeing):"; t; " s."

Print "   Memory sizing (sizing then freeing) using 'Callocate'/'Deallocate':"
t = Timer
For I As Integer = 1 To 10
    Dim As Byte Ptr p = Callocate(100 * 1024 * 1024)
    Deallocate(p)
Next I
t = Timer - t
Print "      total (sizing + freeing):"; t; " s."
Print

Sleep

Output example:

1,000,000 zero-initialized 1KB memory allocations:
   Memory sizing (sizing then freeing) using 'Allocate'+'Clear'/'Deallocate':
      total (sizing + freeing): 0.09319559999858029 s.
   Memory sizing (sizing then freeing) using 'Callocate'/'Deallocate':
      total (sizing + freeing): 0.09936769999330863 s.

10 zero-initialized 100MB memory allocations
   Memory sizing (sizing then freeing) using 'Allocate'+'Clear'/'Deallocate':
      total (sizing + freeing): 0.3598723000031896 s.
   Memory sizing (sizing then freeing) using 'Callocate'/'Deallocate':
      total (sizing + freeing): 0.0001931999868247658 s.

Fix requested:
For New / New[], replace the sequence 'malloc' + 'memset(0)' by 'calloc'.

Discussion

  • fxm (freebasic.net)

    Abstract:

    • The problem is a very significant degradation in the execution time of 'New[]' compared to 'Redim' for a large memory allocation and when the data type has no constructor (and the 'Any' initializer is not used).
    • The analysis showed that this is due to the call to 'alloc' + 'memset(0)' in the 'New' / 'New[]' code, and that for large allocated buffers, the allocated memory provided is already set to zero, and therefore 'memset(0)' is redundant in this case.
    • The fix is simple: For 'New' / 'New[]', replace the sequence 'malloc' + 'memset(0)' by 'calloc' (which does not induce this redundancy), as is already the case for 'Redim'.

    See also this other bug report:
    #749 For UDT with constructor, NEW does not support ANY as initializer while DIM supports this
    Both could be addressed simultaneously.

     

    Last edit: fxm (freebasic.net) 2025-07-30
  • fxm (freebasic.net)

    A fix is proposed by SARG.

     
  • fxm (freebasic.net)

    **In terms of execution time, I find a very significant performance degradation with '-gen gas64' compared to '-gen gcc' only in the following two cases:

    • 'placement New' or 'placement New[]', without 'Any'.
    • 'overload New' or 'overload New[]', without 'Any'.
      ('New' or 'New[]', without 'Any' is OK).**

    Can you look specifically at how memory is zeroed in the '-gen gas64' case compared to the '-gen gcc' case, and this applied to both 'placement New' or 'placement New[]', and 'overload New' or 'overload New[]'.
    (Performance difference between the two instructions types to zero the memory?)

    Test example (the three time values should be about the same):

    Type UDT
        Dim As Integer I
        Declare Operator New[](Byval size As Uinteger) As Any Ptr
        Declare Operator Delete[](Byval buffer As Any Ptr)
    End Type
    Operator UDT.New[](Byval size As Uinteger) As Any Ptr
        Return Allocate(size)
    End Operator
    Operator UDT.Delete[](Byval buffer As Any Ptr)
        Deallocate(buffer)
    End Operator
    
    Print "100,000 zeroed 160KB (20K Integer) memory allocations:"
    Print "  Memory sizing/freeing using 'Allocate'+'Clear' / 'Deallocate':"
    Scope
        Dim As Double t = Timer
        For I As Integer = 1 To 100000
            Dim As Integer Ptr p = Allocate(20 * 1024 * Sizeof(Integer))
            Clear p[0], 0, 20 * 1024 * Sizeof(Integer)
            Deallocate(p)
        Next I
        t = Timer - t
        Print "    total (sizing + freeing):"; t; " s."
    End Scope
    Print "  Memory sizing/freeing using 'Allocate + placement New[]' / 'Deallocate':"
    Scope
        Dim As Double t = Timer
        For I As Integer = 1 To 100000
            Dim As Integer Ptr p = Allocate(20 * 1024 * Sizeof(Integer))
            p = New(p) Integer[20 * 1024]
            Deallocate(p)
        Next I
        t = Timer - t
        Print "    total (sizing + freeing):"; t; " s."
    End Scope
    Print "  Memory sizing/freeing using 'overload New[]' / 'overload Delete[]':"
    Scope
        Dim As Double t = Timer
        For I As Integer = 1 To 100000
            Dim As UDT Ptr p = New UDT[20 * 1024]
            Delete[] p
        Next I
        t = Timer - t
        Print "    total (sizing + freeing):"; t; " s."
    End Scope
    Print
    
    Sleep
    
    • Compiled with '-gen gcc':
    100,000 zeroed 160KB (20K Integer) memory allocations:
      Memory sizing/freeing using 'Allocate'+'Clear' / 'Deallocate':
        total (sizing + freeing): 0.354683900019154 s.          <= OK
      Memory sizing/freeing using 'Allocate + placement New[]' / 'Deallocate':
        total (sizing + freeing): 0.343217299960088 s.          <= OK
      Memory sizing/freeing using 'overload New[]' / 'overload Delete[]':
        total (sizing + freeing): 0.3218252999940887 s.          <= OK
    
    • Compiled with '-gen gas64':
    100,000 zeroed 160KB (20K Integer) memory allocations:
      Memory sizing/freeing using 'Allocate'+'Clear' / 'Deallocate':
        total (sizing + freeing): 0.3360035999794491 s.          <= OK
      Memory sizing/freeing using 'Allocate + placement New[]' / 'Deallocate':
        total (sizing + freeing): 0.6321895999717526 s.          <= NOK
      Memory sizing/freeing using 'overload New[]' / 'overload Delete[]':
        total (sizing + freeing): 0.6090821999823675 s.          <= NOK
    

    Note: This performance difference has existed since the creation of gas64 (it is not a recent degradation because it exists since fbc version 1.08).

    See also from this forum post and the followings:
    https://www.freebasic.net/forum/viewtopic.php?p=308738#p308738

     
  • fxm (freebasic.net)

    A fix is proposed by SARG.

     

Log in to post a comment.

MongoDB Logo MongoDB