• Memory allocators and reporting the allocation size

    From James Harris@james.harris.1@gmail.com to comp.lang.misc on Fri Nov 25 13:02:27 2022
    From Newsgroup: comp.lang.misc

    I will get back to you guys on other topics but I need to improve the
    memory allocator I use in my compiler and that has led to the following
    query.

    Imagine an allocator which carries out plain malloc-type allocations
    (i.e. specified as a number of 8-bit bytes suitably aligned for any
    type) without having to have compatibility with malloc, and in a
    language which is not C.

    For the sake of discussion the set of calls could be

    m_alloc
    m_calloc
    m_realloc
    m_resize
    m_free

    Would there be any value in giving the programmer a way to find out the
    size of a given allocation? I don't mean the size requested but the size allocated (which would be greater than or equal to the size requested).

    I ask because it's simple enough to precede an aligned chunk of memory
    with the allocation size. The allocator may well do that anyway in order
    to help it manage memory. So there seems to be no good reason to keep
    that info from the programmer. The question is over whether there's
    value in allowing the programmer to get such info. I am thinking that he
    could get the value with a call such as

    m_msize(p)

    It seems simple enough, potentially useful, and should be harmless. But
    it's noticeable that the malloc-type calls don't have anything similar.
    So maybe there's a good reason why not.

    I thought it might cause implementation issues such as requiring a
    larger header if the allocator wants to search only free memory. But
    whatever implementation is used I think the allocator will still need to either store or calculate the size of the memory allocated so I am not
    sure I see a problem with the idea.

    What do you guys think? Opinions welcome!
    --
    James Harris

    --- Synchronet 3.19c-Linux NewsLink 1.113
  • From Bart@bc@freeuk.com to comp.lang.misc on Fri Nov 25 14:01:10 2022
    From Newsgroup: comp.lang.misc

    On 25/11/2022 13:02, James Harris wrote:
    I will get back to you guys on other topics but I need to improve the
    memory allocator I use in my compiler and that has led to the following query.

    Imagine an allocator which carries out plain malloc-type allocations
    (i.e. specified as a number of 8-bit bytes suitably aligned for any
    type) without having to have compatibility with malloc, and in a
    language which is not C.

    For the sake of discussion the set of calls could be

      m_alloc
      m_calloc
      m_realloc
      m_resize
      m_free

    Would there be any value in giving the programmer a way to find out the
    size of a given allocation? I don't mean the size requested but the size allocated (which would be greater than or equal to the size requested).

    I ask because it's simple enough to precede an aligned chunk of memory
    with the allocation size. The allocator may well do that anyway in order
    to help it manage memory. So there seems to be no good reason to keep
    that info from the programmer. The question is over whether there's
    value in allowing the programmer to get such info. I am thinking that he could get the value with a call such as

      m_msize(p)

    It seems simple enough, potentially useful, and should be harmless. But
    it's noticeable that the malloc-type calls don't have anything similar.
    So maybe there's a good reason why not.

    I thought it might cause implementation issues such as requiring a
    larger header if the allocator wants to search only free memory. But whatever implementation is used I think the allocator will still need to either store or calculate the size of the memory allocated so I am not
    sure I see a problem with the idea.

    What do you guys think? Opinions welcome!

    The implied assumption here is that the allocator itself needs to
    remember the allocated size, presumably because malloc does so.

    That is different from how any of my own allocators have worked, where
    user programs keep track of the sizes.

    The consequence is that when you call a Free function, you need to
    provide the original size.

    The way malloc works is convenient, especially for larger blocks of
    sizes determined at runtime.

    But for allocating millions of instances of the same small type T, you
    will always know when freeing an instance, that it will have sizeof(T).
    No need for the allocator to waste 8 or more bytes (and messing with alignment) storing that information for an allocation that might not be
    much bigger.

    --- Synchronet 3.19c-Linux NewsLink 1.113
  • From David Brown@david.brown@hesbynett.no to comp.lang.misc on Fri Nov 25 17:15:06 2022
    From Newsgroup: comp.lang.misc

    On 25/11/2022 14:02, James Harris wrote:
    I will get back to you guys on other topics but I need to improve the
    memory allocator I use in my compiler and that has led to the following query.

    Imagine an allocator which carries out plain malloc-type allocations
    (i.e. specified as a number of 8-bit bytes suitably aligned for any
    type) without having to have compatibility with malloc, and in a
    language which is not C.

    For the sake of discussion the set of calls could be

      m_alloc
      m_calloc
      m_realloc
      m_resize
      m_free

    In a new language, I would not follow the names from C as closely. For
    one thing, it could confuse people - they may think they do the same
    thing as in C. For another, "calloc" in particular is a silly name.
    And you don't need "realloc" and "resize".

    It can make sense to distinguish between "alloc_zeroed" and "alloc_unintialised", for performance reasons.


    Would there be any value in giving the programmer a way to find out the
    size of a given allocation? I don't mean the size requested but the size allocated (which would be greater than or equal to the size requested).


    No. It is pointless.

    Why would anyone want to know the result of such a function? The only conceivable thought would be if they had first allocated space for x
    units, and then they now want to store y units - calling "get_real_size"
    could let them know if they need to call "resize" or not.

    The answer is that they should simply call "resize". If "resize" does
    not need to allocate new memory because the real size is big enough, it
    does nothing.

    I ask because it's simple enough to precede an aligned chunk of memory
    with the allocation size. The allocator may well do that anyway in order
    to help it manage memory. So there seems to be no good reason to keep
    that info from the programmer. The question is over whether there's
    value in allowing the programmer to get such info. I am thinking that he could get the value with a call such as

      m_msize(p)

    It seems simple enough, potentially useful, and should be harmless. But
    it's noticeable that the malloc-type calls don't have anything similar.
    So maybe there's a good reason why not.

    I thought it might cause implementation issues such as requiring a
    larger header if the allocator wants to search only free memory. But whatever implementation is used I think the allocator will still need to either store or calculate the size of the memory allocated so I am not
    sure I see a problem with the idea.

    What do you guys think? Opinions welcome!


    I think you should not try to copy C's malloc/free mechanism. In
    particular, I do not think the memory allocator should track the sizes
    of allocations. "free" should always include the size, matching the
    value used in "alloc". (And "resize" should pass the old size as well
    as the new size.)

    Storing the size of allocation was not too bad in earliest days of C,
    with simpler processors, no caches, no multi-threading, and greater
    concern for simple allocation implementations than for performance or reliability. Modern allocators no longer work with a simple linked list storing sizes and link pointers at an address below the address returned
    to the user, so why follow a similar interface?

    Programs know the size of memory they requested when they allocated it.
    They know, almost invariably, the size when they are freeing the
    memory. They know the size of the types, and the size of the arrays.
    So having the memory allocator store this too is a waste.

    Once you've dropped size tracking, you can easily separate allocation
    tracking from the memory in use - this gives neater sizes for pools and
    better cache and virtual page usage. You can have pools of different
    sizes - perhaps 16 byte blocks for the smallest, then 256 byte blocks,
    then 4 KB blocks, etc. (You might prefer different sizes, perhaps with
    more intermediary sizes.) A pool element would have 64 blocks.
    Allocation within a pool is done by quick searches within a single
    64-bit map - no need to search through lists.

    (Of course real implementations involve a fair bit of complexity and
    care, especially for thread safety. But the emphasis must be on cache friendliness.)

    --- Synchronet 3.19c-Linux NewsLink 1.113
  • From Dmitry A. Kazakov@mailbox@dmitry-kazakov.de to comp.lang.misc on Fri Nov 25 18:21:08 2022
    From Newsgroup: comp.lang.misc

    On 2022-11-25 17:15, David Brown wrote:
    On 25/11/2022 14:02, James Harris wrote:

    It can make sense to distinguish between "alloc_zeroed" and "alloc_unintialised", for performance reasons.

    alloc_unmapped/unpaged
    alloc_shared (shared memory pool)
    alloc_local (thread local pool)
    ...

    I think you should not try to copy C's malloc/free mechanism.

    Right. There should be an abstract memory pool interface instead. C
    interface is way too low-level.

    A related important issue is interaction between pointers from different
    pools and universal pointers applicable to all pools including locally allocated objects.

    For example, can a universal pointer be used to deallocate an object? If
    yes, how can it determine the specific pool?
    --
    Regards,
    Dmitry A. Kazakov
    http://www.dmitry-kazakov.de

    --- Synchronet 3.19c-Linux NewsLink 1.113
  • From Bart@bc@freeuk.com to comp.lang.misc on Fri Nov 25 19:23:58 2022
    From Newsgroup: comp.lang.misc

    On 25/11/2022 16:15, David Brown wrote:
    On 25/11/2022 14:02, James Harris wrote:

    Would there be any value in giving the programmer a way to find out
    the size of a given allocation? I don't mean the size requested but
    the size allocated (which would be greater than or equal to the size
    requested).


    No.  It is pointless.

    Why would anyone want to know the result of such a function?  The only conceivable thought would be if they had first allocated space for x
    units, and then they now want to store y units - calling "get_real_size" could let them know if they need to call "resize" or not.

    The answer is that they should simply call "resize".  If "resize" does
    not need to allocate new memory because the real size is big enough, it
    does nothing.

    But who knows how efficient 'resize' will be?

    With `get_real_size`, you do this once when the block is allocated. Then
    you have a simple integer to compare current usage against.

    But there is another reason; take a block which will grow a byte at a
    time. Apart from needing to call 'resize' for every byte, who knows how
    much spare capacity 'resize' will actually provide; it may need to do
    real reallocations more often.

    This C program:

    #include <stdio.h>
    #include <stdlib.h>

    int main(void) {
    char* p;
    int n=1;

    p=malloc(n);

    for(int i=0; i<20000000; ++i) {
    ++n;
    p = realloc(p,n);
    *p = 'A;;
    }
    }

    takes 15 seconds to grow a block a byte at a time to 20M bytes. 10M took
    a 1/4 or that, so it's O(n-squared).

    Contrast that with this /dynamic/ and /interpreted/ loop which grows a
    20M character string a character at a time, but using an internal scheme
    where it controls and can check capacity more efficiently:

    s ::= ""

    to 20 million do
    s +:= 'A'
    od

    This takes 0.3 seconds. Growing to 100M takes 1.4 seconds, so a linear increase; the dumb approach above would take an estimated 6 minutes even executing optimised C code not bytecode.



    --- Synchronet 3.19c-Linux NewsLink 1.113
  • From Bart@bc@freeuk.com to comp.lang.misc on Fri Nov 25 19:46:09 2022
    From Newsgroup: comp.lang.misc

    On 25/11/2022 19:23, Bart wrote:
    On 25/11/2022 16:15, David Brown wrote:

    The answer is that they should simply call "resize".  If "resize" does
    not need to allocate new memory because the real size is big enough,
    it does nothing.

    But who knows how efficient 'resize' will be?

    This takes 0.3 seconds. Growing to 100M takes 1.4 seconds, so a linear increase; the dumb approach above would take an estimated 6 minutes even executing optimised C code not bytecode.

    A C mock-up of the code used within my interpreter manages to grow a
    100MB string in 0.3 seconds, about 1000 times faster than the dumb
    realloc approach.

    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>

    typedef struct {char* ptr; int length, capacity;} string;

    void initstring(string* s) {
    s->ptr=malloc(16);
    s->length=0;
    s->capacity=16;
    }

    void appendchar(string* s, int c) {
    int newcap;
    if (s->length >= s->capacity) {
    newcap = s->capacity*2;
    s->ptr = realloc(s->ptr, newcap);
    s->capacity = newcap;
    }

    *(s->ptr + s->length) = c;
    ++(s->length);
    }

    int main(void) {
    string s;
    initstring(&s);

    for(int i=0; i<100000000; ++i) {
    appendchar(&s, 'A');
    }

    appendchar(&s, 0);
    printf("%lld\n", strlen(s.ptr));
    }


    --- Synchronet 3.19c-Linux NewsLink 1.113
  • From James Harris@james.harris.1@gmail.com to comp.lang.misc on Sat Nov 26 03:31:42 2022
    From Newsgroup: comp.lang.misc

    On Friday, 25 November 2022 at 14:01:12 UTC, Bart wrote:
    On 25/11/2022 13:02, James Harris wrote:
    I will get back to you guys on other topics but I need to improve the memory allocator I use in my compiler and that has led to the following query.

    Imagine an allocator which carries out plain malloc-type allocations
    (i.e. specified as a number of 8-bit bytes suitably aligned for any
    type) without having to have compatibility with malloc, and in a
    language which is not C.

    For the sake of discussion the set of calls could be

    m_alloc
    m_calloc
    m_realloc
    m_resize
    m_free

    Would there be any value in giving the programmer a way to find out the size of a given allocation? I don't mean the size requested but the size allocated (which would be greater than or equal to the size requested).

    I ask because it's simple enough to precede an aligned chunk of memory with the allocation size. The allocator may well do that anyway in order to help it manage memory. So there seems to be no good reason to keep
    that info from the programmer. The question is over whether there's
    value in allowing the programmer to get such info. I am thinking that he could get the value with a call such as

    m_msize(p)

    It seems simple enough, potentially useful, and should be harmless. But it's noticeable that the malloc-type calls don't have anything similar.
    So maybe there's a good reason why not.

    I thought it might cause implementation issues such as requiring a
    larger header if the allocator wants to search only free memory. But whatever implementation is used I think the allocator will still need to either store or calculate the size of the memory allocated so I am not sure I see a problem with the idea.

    What do you guys think? Opinions welcome!
    The implied assumption here is that the allocator itself needs to
    remember the allocated size, presumably because malloc does so.

    That is different from how any of my own allocators have worked, where
    user programs keep track of the sizes.

    The consequence is that when you call a Free function, you need to
    provide the original size.

    The way malloc works is convenient, especially for larger blocks of
    sizes determined at runtime.

    But for allocating millions of instances of the same small type T, you
    will always know when freeing an instance, that it will have sizeof(T).
    No need for the allocator to waste 8 or more bytes (and messing with alignment) storing that information for an allocation that might not be
    much bigger.
    It's an interesting topic. I can think of three really fast memory allocation approaches:
    1. sbrk-like
    2. stack
    3. array of slots, with occupied/vacant essentially indicated by a bitmap.
    The sbrk-like approach (taking memory as needed and not returning it) can work well if all allocations can remain until the program terminates.
    The stack approach can work well if allocations can be deallocated in reverse order.
    The array of slots can work well if the things to allocate are of the same size.
    Unfortunately, none of those work well for the dynamic creation and destruction of strings - which is what I need just now. The malloc-type approach is the most flexible. While it's not the fastest it is the most generally useful. The idea of giving the allocator the ability to return the allocation size would be akin to it returning the 'capacity'.
    Your suggestion of requiring the caller to remember how large the allocation is is intriguing and it sounds rather like what mmap expects. But for strings how would it work, especially if a string's length is not known in advance? Further, what if the caller requested 100 bytes then told the allocator to free 99 bytes? What if it told the allocator to free 101 bytes?
    --
    James
    --- Synchronet 3.19c-Linux NewsLink 1.113
  • From Dmitry A. Kazakov@mailbox@dmitry-kazakov.de to comp.lang.misc on Sat Nov 26 12:57:10 2022
    From Newsgroup: comp.lang.misc

    On 2022-11-26 12:31, James Harris wrote:

    It's an interesting topic. I can think of three really fast memory allocation approaches:

    1. sbrk-like
    2. stack
    3. array of slots, with occupied/vacant essentially indicated by a bitmap.

    You should really look at existing algorithms and implementations of
    memory pools. There is a huge amount of literature and research done on
    the subject with different goals set as there is no best memory pool management algorithm for all purposes.

    Unfortunately, none of those work well for the dynamic creation and destruction of strings - which is what I need just now.

    Then memory fragmentation must be your main concern. For strings some prediction heuristics are used both at the pool level as in the dynamic
    string implementation. Usually more memory allocated than needed in
    order to anticipate further growth.

    BTW, since any reasonable pool implementation provides some pool walking
    API as well as allocation/deallocation hooks for debugging purpose,
    clearly, the block size and other pool management stuff must be
    available anyway.

    E.g. see:

    https://learn.microsoft.com/en-us/windows/win32/toolhelp/heap-lists-and-heap-walking
    --
    Regards,
    Dmitry A. Kazakov
    http://www.dmitry-kazakov.de

    --- Synchronet 3.19c-Linux NewsLink 1.113
  • From James Harris@james.harris.1@gmail.com to comp.lang.misc on Sat Nov 26 05:05:46 2022
    From Newsgroup: comp.lang.misc

    On Friday, 25 November 2022 at 16:15:08 UTC, David Brown wrote:
    On 25/11/2022 14:02, James Harris wrote:
    I will get back to you guys on other topics but I need to improve the memory allocator I use in my compiler and that has led to the following query.

    Imagine an allocator which carries out plain malloc-type allocations
    (i.e. specified as a number of 8-bit bytes suitably aligned for any
    type) without having to have compatibility with malloc, and in a
    language which is not C.

    For the sake of discussion the set of calls could be

    m_alloc
    m_calloc
    m_realloc
    m_resize
    m_free
    In a new language, I would not follow the names from C as closely. For
    one thing, it could confuse people - they may think they do the same
    thing as in C. For another, "calloc" in particular is a silly name.
    And you don't need "realloc" and "resize".
    The idea is that realloc could move the allocation whereas resize could only resize it in place. The latter is not present in C's malloc family.

    It can make sense to distinguish between "alloc_zeroed" and "alloc_unintialised", for performance reasons.
    Agreed, but what exactly would you want for the calls? Maybe
    m_alloc(size)
    m_alloc_a(size, flags)
    ?

    Would there be any value in giving the programmer a way to find out the size of a given allocation? I don't mean the size requested but the size allocated (which would be greater than or equal to the size requested).

    No. It is pointless.

    Why would anyone want to know the result of such a function? The only conceivable thought would be if they had first allocated space for x
    units, and then they now want to store y units - calling "get_real_size" could let them know if they need to call "resize" or not.

    The answer is that they should simply call "resize". If "resize" does
    not need to allocate new memory because the real size is big enough, it
    does nothing.
    It's partly for performance, as Bart's comments have backed up. If code can find out the capacity then it can fill that allocation up to the stated capacity without having to make any other calls and without risking moving what has been stored so far.
    I ask because it's simple enough to precede an aligned chunk of memory with the allocation size. The allocator may well do that anyway in order to help it manage memory. So there seems to be no good reason to keep
    that info from the programmer. The question is over whether there's
    value in allowing the programmer to get such info. I am thinking that he could get the value with a call such as

    m_msize(p)

    It seems simple enough, potentially useful, and should be harmless. But it's noticeable that the malloc-type calls don't have anything similar.
    So maybe there's a good reason why not.

    I thought it might cause implementation issues such as requiring a
    larger header if the allocator wants to search only free memory. But whatever implementation is used I think the allocator will still need to either store or calculate the size of the memory allocated so I am not sure I see a problem with the idea.

    What do you guys think? Opinions welcome!

    I think you should not try to copy C's malloc/free mechanism. In
    particular, I do not think the memory allocator should track the sizes
    of allocations. "free" should always include the size, matching the
    value used in "alloc". (And "resize" should pass the old size as well
    as the new size.)
    Bart said the same and the suggestion surprises me. It seems prone to error.

    Storing the size of allocation was not too bad in earliest days of C,
    with simpler processors, no caches, no multi-threading, and greater
    concern for simple allocation implementations than for performance or reliability. Modern allocators no longer work with a simple linked list storing sizes and link pointers at an address below the address returned
    to the user, so why follow a similar interface?

    Programs know the size of memory they requested when they allocated it.
    They know, almost invariably, the size when they are freeing the
    memory. They know the size of the types, and the size of the arrays.
    So having the memory allocator store this too is a waste.
    That's sometimes the case but not always. Say I wanted to read a line from a file a byte at a time without knowing in advance how long the line would be (a common-enough requirement but one which C programs all too often fudge by defining a maximum line length). The required allocation could not be known in advance.
    There are various specialised allocation techniques but the malloc style is the most general I know of.

    Once you've dropped size tracking, you can easily separate allocation tracking from the memory in use - this gives neater sizes for pools and better cache and virtual page usage. You can have pools of different
    sizes - perhaps 16 byte blocks for the smallest, then 256 byte blocks,
    then 4 KB blocks, etc. (You might prefer different sizes, perhaps with
    more intermediary sizes.) A pool element would have 64 blocks.
    Allocation within a pool is done by quick searches within a single
    64-bit map - no need to search through lists.
    I presume by pools you mean slots of a certain size I may, longer term, let a program specify slot sizes and pool geometry. But that's for the future and it wouldn't work well with variable-length strings, especially where the length is not known in advance. So I need something simple and general for now.

    (Of course real implementations involve a fair bit of complexity and
    care, especially for thread safety. But the emphasis must be on cache friendliness.)
    Agreed.
    --
    James
    --- Synchronet 3.19c-Linux NewsLink 1.113
  • From James Harris@james.harris.1@gmail.com to comp.lang.misc on Sat Nov 26 05:15:03 2022
    From Newsgroup: comp.lang.misc

    On Friday, 25 November 2022 at 17:21:11 UTC, Dmitry A. Kazakov wrote:
    On 2022-11-25 17:15, David Brown wrote:
    On 25/11/2022 14:02, James Harris wrote:

    It can make sense to distinguish between "alloc_zeroed" and "alloc_unintialised", for performance reasons.
    alloc_unmapped/unpaged
    alloc_shared (shared memory pool)
    alloc_local (thread local pool)
    ...
    I think you should not try to copy C's malloc/free mechanism.
    Right. There should be an abstract memory pool interface instead. C interface is way too low-level.
    As I said to David, what about strings, especially if their length is not known in advance? What's your preferred model for manipulating them? The malloc-style calls are not perfect but they do support any such memory handling.

    A related important issue is interaction between pointers from different pools and universal pointers applicable to all pools including locally allocated objects.
    David was talking about, AIUI, pools of slots where each slot had the same size. You seem to be talking about pools being 'NUMA pools' where each pool is of bytes but has certain access characteristics such as bandwidth, latency, and contention. Is that right?

    For example, can a universal pointer be used to deallocate an object? If yes, how can it determine the specific pool?
    I am not sure what that means. Presumably addresses can have two parts: NUMA pool and offset within the pool.
    --
    James
    --- Synchronet 3.19c-Linux NewsLink 1.113
  • From Bart@bc@freeuk.com to comp.lang.misc on Sat Nov 26 13:18:52 2022
    From Newsgroup: comp.lang.misc

    On 26/11/2022 11:31, James Harris wrote:
    On Friday, 25 November 2022 at 14:01:12 UTC, Bart wrote:

    That is different from how any of my own allocators have worked, where
    user programs keep track of the sizes.

    The consequence is that when you call a Free function, you need to
    provide the original size.

    The way malloc works is convenient, especially for larger blocks of
    sizes determined at runtime.

    But for allocating millions of instances of the same small type T, you
    will always know when freeing an instance, that it will have sizeof(T).
    No need for the allocator to waste 8 or more bytes (and messing with
    alignment) storing that information for an allocation that might not be
    much bigger.

    It's an interesting topic. I can think of three really fast memory allocation approaches:

    1. sbrk-like
    2. stack
    3. array of slots, with occupied/vacant essentially indicated by a bitmap.

    The sbrk-like approach (taking memory as needed and not returning it) can work well if all allocations can remain until the program terminates.

    The stack approach can work well if allocations can be deallocated in reverse order.

    The array of slots can work well if the things to allocate are of the same size.

    Unfortunately, none of those work well for the dynamic creation and destruction of strings - which is what I need just now. The malloc-type approach is the most flexible. While it's not the fastest it is the most generally useful. The idea of giving the allocator the ability to return the allocation size would be akin to it returning the 'capacity'.

    Your suggestion of requiring the caller to remember how large the allocation is is intriguing and it sounds rather like what mmap expects. But for strings how would it work, especially if a string's length is not known in advance?

    How would strings work now with malloc? That still requires the length!

    If you have counted strings, then you will know their length to free
    them. Otherwise use strlen. But /something/ has to record the length.

    You could use special routines just for strings, that say store a length
    in the space just before the returned pointer. Then you can also have a routine to return that length, saving you some hassle. But all you've
    done now really is create a mini string library.

    Further, what if the caller requested 100 bytes then told the allocator to free 99 bytes? What if it told the allocator to free 101 bytes?

    Then it'll go wrong. You could also pass the wrong pointer to the
    deallocator. Or allocate the space in the first place with 99 instead of
    100; what stops you doing that?

    You can always create a couple of routines on top which will allocate 8
    extra bytes to store the requested length, similar to the idea for
    strings. This is purely for convenience.

    It's also quite easy to create custom allocators for special purposes.
    For example if you are dealing with millions of identical small objects
    which are being constantly created and destroyed, allocate a large pool
    of memory to hold them.

    Then all freed objects can be linked together; allocating a new object
    is very fast, and deallocating instant: add this object to the head of
    the free-list.

    You can also deallocate the lot in one go.



    --- Synchronet 3.19c-Linux NewsLink 1.113
  • From Dmitry A. Kazakov@mailbox@dmitry-kazakov.de to comp.lang.misc on Sat Nov 26 14:46:09 2022
    From Newsgroup: comp.lang.misc

    On 2022-11-26 14:15, James Harris wrote:
    On Friday, 25 November 2022 at 17:21:11 UTC, Dmitry A. Kazakov wrote:
    On 2022-11-25 17:15, David Brown wrote:
    On 25/11/2022 14:02, James Harris wrote:

    It can make sense to distinguish between "alloc_zeroed" and
    "alloc_unintialised", for performance reasons.
    alloc_unmapped/unpaged
    alloc_shared (shared memory pool)
    alloc_local (thread local pool)
    ...
    I think you should not try to copy C's malloc/free mechanism.
    Right. There should be an abstract memory pool interface instead. C
    interface is way too low-level.

    As I said to David, what about strings, especially if their length is not known in advance? What's your preferred model for manipulating them? The malloc-style calls are not perfect but they do support any such memory handling.

    Usual implementation of a *contiguous* string is that when truncated,
    the memory is not reclaimed. When expanded, it is done by some chunks.
    For example by 100 bytes. Or by some linear function of current size
    etc. On top of that sits reference counting that prevents cloning the
    string body when there is only one reference to it.

    Note that memory pools apply similar techniques as well. You can check
    clib memory pool to see that it allocates more than you actually request
    even excluding block management stuff.

    I do not know why you care about it. Normally the pools of the language run-time are backed by the system pool. E.g. Ada's standard memory pool allocates in clib pool under Linux. Your concern must be allowing
    programmer to allocate objects in custom pools in some unified way. E.g.
    my components library provides pools shared by multiple processes, pools persistent between application runs etc. This is possible in Ada because pointer types there are associated with a pool. So when you write

    P := new Employee_Record;

    it dispatches according to the type of the pool associated with type of
    the pointer P. That calls method Allocate the pool must provide.

    A related important issue is interaction between pointers from different
    pools and universal pointers applicable to all pools including locally
    allocated objects.

    David was talking about, AIUI, pools of slots where each slot had the same size.

    This one of many techniques to implement a pool, e.g. as a set of lists
    of blocks of same size, usually power of two.

    You seem to be talking about pools being 'NUMA pools' where each pool is of bytes but has certain access characteristics such as bandwidth, latency, and contention. Is that right?

    Just a pool = heap. E.g. under Windows an application may have a number
    of different heaps.

    For example, can a universal pointer be used to deallocate an object? If
    yes, how can it determine the specific pool?

    I am not sure what that means. Presumably addresses can have two parts: NUMA pool and offset within the pool.

    You cannot do that because a universal pointer can point anywhere, to
    the thread local storage for example, and because pools are rarely
    contiguous in the process address space. Therefore in this model the
    universal pointer must be fatter than the machine address:

    - address of the pool head
    - address of the object

    All conversions between pointers must carry, remove, add pool head address.
    --
    Regards,
    Dmitry A. Kazakov
    http://www.dmitry-kazakov.de

    --- Synchronet 3.19c-Linux NewsLink 1.113
  • From BGB@cr88192@gmail.com to comp.lang.misc on Sat Nov 26 16:22:18 2022
    From Newsgroup: comp.lang.misc

    On 11/25/2022 8:01 AM, Bart wrote:
    On 25/11/2022 13:02, James Harris wrote:
    I will get back to you guys on other topics but I need to improve the
    memory allocator I use in my compiler and that has led to the
    following query.

    Imagine an allocator which carries out plain malloc-type allocations
    (i.e. specified as a number of 8-bit bytes suitably aligned for any
    type) without having to have compatibility with malloc, and in a
    language which is not C.

    For the sake of discussion the set of calls could be

       m_alloc
       m_calloc
       m_realloc
       m_resize
       m_free

    Would there be any value in giving the programmer a way to find out
    the size of a given allocation? I don't mean the size requested but
    the size allocated (which would be greater than or equal to the size
    requested).

    I ask because it's simple enough to precede an aligned chunk of memory
    with the allocation size. The allocator may well do that anyway in
    order to help it manage memory. So there seems to be no good reason to
    keep that info from the programmer. The question is over whether
    there's value in allowing the programmer to get such info. I am
    thinking that he could get the value with a call such as

       m_msize(p)

    It seems simple enough, potentially useful, and should be harmless.
    But it's noticeable that the malloc-type calls don't have anything
    similar. So maybe there's a good reason why not.

    I thought it might cause implementation issues such as requiring a
    larger header if the allocator wants to search only free memory. But
    whatever implementation is used I think the allocator will still need
    to either store or calculate the size of the memory allocated so I am
    not sure I see a problem with the idea.

    What do you guys think? Opinions welcome!

    The implied assumption here is that the allocator itself needs to
    remember the allocated size, presumably because malloc does so.

    That is different from how any of my own allocators have worked, where
    user programs keep track of the sizes.

    The consequence is that when you call a Free function, you need to
    provide the original size.

    The way malloc works is convenient, especially for larger blocks of
    sizes determined at runtime.

    But for allocating millions of instances of the same small type T, you
    will always know when freeing an instance, that it will have sizeof(T).
    No need for the allocator to waste 8 or more bytes (and messing with alignment) storing that information for an allocation that might not be
    much bigger.


    In my allocator designs, I don't spend 8 bytes on remembering the object
    size, but rather 8 bits, with the size typically stored as an E5.F3
    microfloat style format (sort of like A-law).

    This size value is also used as a key to the corresponding free-list
    (with all objects in a given free-list being the same size in memory).

    Say, object sizes are N*16, where N is encoded in a microfloat:
    00..07: 0000..0007
    08..0F: 0008..000F
    10..17: 0010..001E
    18..1F: 0020..003C
    20..27: 0040..0078
    28..2F: 0080..00F0
    30..37: 0100..01E0
    38..3F: 0200..03C0
    40..47: 0400..0780
    48..4F: 0800..0F00
    50..57: 1000..1E00
    58..5F: 2000..3C00
    60..67: 4000..7800
    68..6F: 8000..F000
    ...

    So, say, '68' here maps to an object holding 512K, ...


    This scheme could theoretically express object sizes up to around 256GB.

    Except that by this point, we have switched to spans of pages, which
    store a page count rather than expressing their size using an 8-bit microfloat.


    --- Synchronet 3.19c-Linux NewsLink 1.113
  • From Bart@bc@freeuk.com to comp.lang.misc on Sun Nov 27 00:02:52 2022
    From Newsgroup: comp.lang.misc

    On 26/11/2022 22:22, BGB wrote:
    On 11/25/2022 8:01 AM, Bart wrote:
    On 25/11/2022 13:02, James Harris wrote:
    I will get back to you guys on other topics but I need to improve the
    memory allocator I use in my compiler and that has led to the
    following query.

    Imagine an allocator which carries out plain malloc-type allocations
    (i.e. specified as a number of 8-bit bytes suitably aligned for any
    type) without having to have compatibility with malloc, and in a
    language which is not C.

    For the sake of discussion the set of calls could be

       m_alloc
       m_calloc
       m_realloc
       m_resize
       m_free

    Would there be any value in giving the programmer a way to find out
    the size of a given allocation? I don't mean the size requested but
    the size allocated (which would be greater than or equal to the size
    requested).

    I ask because it's simple enough to precede an aligned chunk of
    memory with the allocation size. The allocator may well do that
    anyway in order to help it manage memory. So there seems to be no
    good reason to keep that info from the programmer. The question is
    over whether there's value in allowing the programmer to get such
    info. I am thinking that he could get the value with a call such as

       m_msize(p)

    It seems simple enough, potentially useful, and should be harmless.
    But it's noticeable that the malloc-type calls don't have anything
    similar. So maybe there's a good reason why not.

    I thought it might cause implementation issues such as requiring a
    larger header if the allocator wants to search only free memory. But
    whatever implementation is used I think the allocator will still need
    to either store or calculate the size of the memory allocated so I am
    not sure I see a problem with the idea.

    What do you guys think? Opinions welcome!

    The implied assumption here is that the allocator itself needs to
    remember the allocated size, presumably because malloc does so.

    That is different from how any of my own allocators have worked, where
    user programs keep track of the sizes.

    The consequence is that when you call a Free function, you need to
    provide the original size.

    The way malloc works is convenient, especially for larger blocks of
    sizes determined at runtime.

    But for allocating millions of instances of the same small type T, you
    will always know when freeing an instance, that it will have
    sizeof(T). No need for the allocator to waste 8 or more bytes (and
    messing with alignment) storing that information for an allocation
    that might not be much bigger.


    In my allocator designs, I don't spend 8 bytes on remembering the object size, but rather 8 bits, with the size typically stored as an E5.F3 microfloat style format (sort of like A-law).

    This size value is also used as a key to the corresponding free-list
    (with all objects in a given free-list being the same size in memory).

    Say, object sizes are N*16, where N is encoded in a microfloat:
      00..07: 0000..0007
      08..0F: 0008..000F
      10..17: 0010..001E
      18..1F: 0020..003C
      20..27: 0040..0078
      28..2F: 0080..00F0
      30..37: 0100..01E0
      38..3F: 0200..03C0
      40..47: 0400..0780
      48..4F: 0800..0F00
      50..57: 1000..1E00
      58..5F: 2000..3C00
      60..67: 4000..7800
      68..6F: 8000..F000
      ...

    So, say, '68' here maps to an object holding 512K, ...


    This scheme could theoretically express object sizes up to around 256GB.

    Except that by this point, we have switched to spans of pages, which
    store a page count rather than expressing their size using an 8-bit microfloat.

    I have used a scheme where an 8-bit code represented the allocated size
    of an object, as there was no room to store the full 32-bit value.

    The actual byte-size was obtained with a lookup table; allocated sizes
    started off doubling in size up to a point (32MB?), then increased
    linearly, up to a maximum of just under 2GB.

    But, within an actual allocator, storing a single byte is going to cause problems, because it will throw out the alighment; you might need to
    round up to 64 bits anyway (on 64-bit systems). Unless you stored these
    bytes in a separate location, which then makes it all more complex.

    As I said, I currently store zero bytes with each allocation, which has
    no effect on alignment!





    --- Synchronet 3.19c-Linux NewsLink 1.113
  • From BGB@cr88192@gmail.com to comp.lang.misc on Sun Nov 27 01:16:50 2022
    From Newsgroup: comp.lang.misc

    On 11/26/2022 6:02 PM, Bart wrote:
    On 26/11/2022 22:22, BGB wrote:
    On 11/25/2022 8:01 AM, Bart wrote:
    On 25/11/2022 13:02, James Harris wrote:
    I will get back to you guys on other topics but I need to improve
    the memory allocator I use in my compiler and that has led to the
    following query.

    Imagine an allocator which carries out plain malloc-type allocations
    (i.e. specified as a number of 8-bit bytes suitably aligned for any
    type) without having to have compatibility with malloc, and in a
    language which is not C.

    For the sake of discussion the set of calls could be

       m_alloc
       m_calloc
       m_realloc
       m_resize
       m_free

    Would there be any value in giving the programmer a way to find out
    the size of a given allocation? I don't mean the size requested but
    the size allocated (which would be greater than or equal to the size
    requested).

    I ask because it's simple enough to precede an aligned chunk of
    memory with the allocation size. The allocator may well do that
    anyway in order to help it manage memory. So there seems to be no
    good reason to keep that info from the programmer. The question is
    over whether there's value in allowing the programmer to get such
    info. I am thinking that he could get the value with a call such as

       m_msize(p)

    It seems simple enough, potentially useful, and should be harmless.
    But it's noticeable that the malloc-type calls don't have anything
    similar. So maybe there's a good reason why not.

    I thought it might cause implementation issues such as requiring a
    larger header if the allocator wants to search only free memory. But
    whatever implementation is used I think the allocator will still
    need to either store or calculate the size of the memory allocated
    so I am not sure I see a problem with the idea.

    What do you guys think? Opinions welcome!

    The implied assumption here is that the allocator itself needs to
    remember the allocated size, presumably because malloc does so.

    That is different from how any of my own allocators have worked,
    where user programs keep track of the sizes.

    The consequence is that when you call a Free function, you need to
    provide the original size.

    The way malloc works is convenient, especially for larger blocks of
    sizes determined at runtime.

    But for allocating millions of instances of the same small type T,
    you will always know when freeing an instance, that it will have
    sizeof(T). No need for the allocator to waste 8 or more bytes (and
    messing with alignment) storing that information for an allocation
    that might not be much bigger.


    In my allocator designs, I don't spend 8 bytes on remembering the
    object size, but rather 8 bits, with the size typically stored as an
    E5.F3 microfloat style format (sort of like A-law).

    This size value is also used as a key to the corresponding free-list
    (with all objects in a given free-list being the same size in memory).

    Say, object sizes are N*16, where N is encoded in a microfloat:
       00..07: 0000..0007
       08..0F: 0008..000F
       10..17: 0010..001E
       18..1F: 0020..003C
       20..27: 0040..0078
       28..2F: 0080..00F0
       30..37: 0100..01E0
       38..3F: 0200..03C0
       40..47: 0400..0780
       48..4F: 0800..0F00
       50..57: 1000..1E00
       58..5F: 2000..3C00
       60..67: 4000..7800
       68..6F: 8000..F000
       ...

    So, say, '68' here maps to an object holding 512K, ...


    This scheme could theoretically express object sizes up to around 256GB.

    Except that by this point, we have switched to spans of pages, which
    store a page count rather than expressing their size using an 8-bit
    microfloat.

    I have used a scheme where an 8-bit code represented the allocated size
    of an object, as there was no room to store the full 32-bit value.

    The actual byte-size was obtained with a lookup table; allocated sizes started off doubling in size up to a point (32MB?), then increased
    linearly, up to a maximum of just under 2GB.

    But, within an actual allocator, storing a single byte is going to cause problems, because it will throw out the alighment; you might need to
    round up to 64 bits anyway (on 64-bit systems). Unless you stored these bytes in a separate location, which then makes it all more complex.

    As I said, I currently store zero bytes with each allocation, which has
    no effect on alignment!


    In my case, there is also type-tag and zone-ID information in the object headers.

    Say:
    Object size byte;
    Object mode/flag byte;
    Type-Tag as a 16 bit word;
    Zone-ID as a 16-bit word;
    ...

    Heap objects generally have a 16 byte alignment in my case.


    For larger objects (heap chunks and page-allocated data), typically the metadata about the object is stored externally.

    I had considered switching to external metadata for medium objects, but
    have not done so. For small objects (which are allocated using a cell
    bitmap; 16 bytes at a time), there is no good alternative to using
    inline headers (typically, the first cell of the object is dedicated as
    a header cell).


    For some special object types (such as "cons cells") use a separate
    allocator mechanism that does not use per-object headers. However, the
    size and type of each such object is fixed (for example, all cons cells
    are 16 bytes; and currently cons cells may not be assigned to zones).

    The "zone" is basically a mechanism where a zone ID can be assigned to
    an object, and then all objects with zone ID's matching a given pattern
    can be bulk freed.


    --- Synchronet 3.19c-Linux NewsLink 1.113
  • From David Brown@david.brown@hesbynett.no to comp.lang.misc on Sun Nov 27 16:18:14 2022
    From Newsgroup: comp.lang.misc

    On 25/11/2022 20:23, Bart wrote:
    On 25/11/2022 16:15, David Brown wrote:
    On 25/11/2022 14:02, James Harris wrote:

    Would there be any value in giving the programmer a way to find out
    the size of a given allocation? I don't mean the size requested but
    the size allocated (which would be greater than or equal to the size
    requested).


    No.  It is pointless.

    Why would anyone want to know the result of such a function?  The only
    conceivable thought would be if they had first allocated space for x
    units, and then they now want to store y units - calling
    "get_real_size" could let them know if they need to call "resize" or not.

    The answer is that they should simply call "resize".  If "resize" does
    not need to allocate new memory because the real size is big enough,
    it does nothing.

    But who knows how efficient 'resize' will be?

    With `get_real_size`, you do this once when the block is allocated. Then
    you have a simple integer to compare current usage against.


    A "resize" function would always have code equivalent to first calling "get_real_size" and then seeing if an actual re-allocation and copy is
    needed. It would be pointless for user code to duplicate this
    functionality. Therefore, it is pointless to have a "get_real_size"
    function available to the user code - using it would be less efficient
    than simply calling "resize" directly.


    But there is another reason; take a block which will grow a byte at a
    time. Apart from needing to call 'resize' for every byte, who knows how
    much spare capacity 'resize' will actually provide; it may need to do
    real reallocations more often.

    This C program:

        #include <stdio.h>
        #include <stdlib.h>

        int main(void) {
            char* p;
            int n=1;

            p=malloc(n);

            for(int i=0; i<20000000; ++i) {
                ++n;
                p = realloc(p,n);
                *p = 'A;;
            }
        }


    Trying to optimise your interface around pathological examples is
    madness. Design the interface to be useful for real, practical code.


    --- Synchronet 3.19c-Linux NewsLink 1.113
  • From Bart@bc@freeuk.com to comp.lang.misc on Sun Nov 27 15:47:55 2022
    From Newsgroup: comp.lang.misc

    On 27/11/2022 15:18, David Brown wrote:
    On 25/11/2022 20:23, Bart wrote:
    On 25/11/2022 16:15, David Brown wrote:
    On 25/11/2022 14:02, James Harris wrote:

    Would there be any value in giving the programmer a way to find out
    the size of a given allocation? I don't mean the size requested but
    the size allocated (which would be greater than or equal to the size
    requested).


    No.  It is pointless.

    Why would anyone want to know the result of such a function?  The
    only conceivable thought would be if they had first allocated space
    for x units, and then they now want to store y units - calling
    "get_real_size" could let them know if they need to call "resize" or
    not.

    The answer is that they should simply call "resize".  If "resize"
    does not need to allocate new memory because the real size is big
    enough, it does nothing.

    But who knows how efficient 'resize' will be?

    With `get_real_size`, you do this once when the block is allocated.
    Then you have a simple integer to compare current usage against.


    A "resize" function would always have code equivalent to first calling "get_real_size" and then seeing if an actual re-allocation and copy is needed.  It would be pointless for user code to duplicate this functionality.  Therefore, it is pointless to have a "get_real_size" function available to the user code - using it would be less efficient
    than simply calling "resize" directly.


    But there is another reason; take a block which will grow a byte at a
    time. Apart from needing to call 'resize' for every byte, who knows
    how much spare capacity 'resize' will actually provide; it may need to
    do real reallocations more often.

    This C program:

         #include <stdio.h>
         #include <stdlib.h>

         int main(void) {
             char* p;
             int n=1;

             p=malloc(n);

             for(int i=0; i<20000000; ++i) {
                 ++n;
                 p = realloc(p,n);
                 *p = 'A;;
             }
         }


    Trying to optimise your interface around pathological examples is
    madness.  Design the interface to be useful for real, practical code.

    So why do you think realloc is so slow in this context, compared with
    how realloc was called in my other post? (Repeated below for convenience.)

    That is, the above dumb use of realloc shows O(n^2) behaviour compared
    with the version below which is O(n). Because you are saying above that
    it should be just as efficient.

    People do have memory blocks that can grow by small amounts, or a byte
    at a time; how do you think such growth should be implemented, by
    millions of calls to realloc for a byte-at-a-time reallocation, or
    something a bit cleverer?

    It's that 'a bit cleverer' part that was the purpose of 'get_real_size', except I would go further.




    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>

    typedef struct {char* ptr; int length, capacity;} string;

    void initstring(string* s) {
    s->ptr=malloc(16);
    s->length=0;
    s->capacity=16;
    }

    void appendchar(string* s, int c) {
    int newcap;
    if (s->length >= s->capacity) {
    newcap = s->capacity*2;
    s->ptr = realloc(s->ptr, newcap);
    s->capacity = newcap;
    }

    *(s->ptr + s->length) = c;
    ++(s->length);
    }

    int main(void) {
    string s;
    initstring(&s);

    for(int i=0; i<100000000; ++i) {
    appendchar(&s, 'A');
    }

    appendchar(&s, 0);
    printf("%lld\n", strlen(s.ptr));
    }


    --- Synchronet 3.19c-Linux NewsLink 1.113
  • From Andy Walker@anw@cuboid.co.uk to comp.lang.misc on Sun Nov 27 17:06:00 2022
    From Newsgroup: comp.lang.misc

    On 25/11/2022 16:15, David Brown wrote:
    I think you should not try to copy C's malloc/free mechanism.

    Perhaps worth noting that there are two different sorts of "free" mechanism:

    (a) When the /programmer/ decides that some storage is no longed needed
    and therefore decides it can be re-used. Obviously, there are some
    applications where this is reliable. But in general it is unsafe.
    The problems arise when there are graph structures where nodes of
    the graph may be pointed at directly or indirectly by virtue of
    some pointer accessing a component of the node. This is difficult
    programming, and programmers often get it wrong, leading to perhaps
    subtle bugs, or else don't bother, leading to poor performance as
    storage is retained that could be freed.

    (b) When the storage manager for the compiler/library decides that some
    storage is no-longer needed. IOW, when garbage collection, or near
    equivalent, re-organises storage and perhaps returns some to the
    OS. This is also difficult, but is a solved problem so should be
    tried and tested for your language and thus reliable.

    Summary: You will deduce that my opinion is that systems/languages
    that rely on people to write programs that get "free" right are a
    disaster waiting to happen You could perhaps regard "free" as a hint
    that you expect storage to be garbage-collectible, but the final
    decision should always be taken by a proper garbage collector.
    --
    Andy Walker, Nottingham.
    Andy's music pages: www.cuboid.me.uk/andy/Music
    Composer of the day: www.cuboid.me.uk/andy/Music/Composers/Hummel
    --- Synchronet 3.19c-Linux NewsLink 1.113
  • From David Brown@david.brown@hesbynett.no to comp.lang.misc on Sun Nov 27 18:11:59 2022
    From Newsgroup: comp.lang.misc

    On 27/11/2022 16:47, Bart wrote:
    On 27/11/2022 15:18, David Brown wrote:

    Trying to optimise your interface around pathological examples is
    madness.  Design the interface to be useful for real, practical code.

    So why do you think realloc is so slow in this context, compared with
    how realloc was called in my other post? (Repeated below for convenience.)

    That is, the above dumb use of realloc shows O(n^2) behaviour compared
    with the version below which is O(n). Because you are saying above that
    it should be just as efficient.


    No - I am saying that using some kind of "get_real_size" gives you nothing.

    Only an idiot would call realloc() or anything remotely like that a byte
    at a time. (We are talking about low-level languages here - in a high
    level language, you might append to a string or list one element at a
    time, expecting the language to implement things efficiently.)

    People do have memory blocks that can grow by small amounts, or a byte
    at a time; how do you think such growth should be implemented, by
    millions of calls to realloc for a byte-at-a-time reallocation, or
    something a bit cleverer?

    It's that 'a bit cleverer' part that was the purpose of 'get_real_size', except I would go further.

    The code that tracks capacity is vastly cleverer than code that
    allocates a byte at a time. "get_real_size" would add /nothing/ to it, because you already track the capacity - and it is more efficient to do
    so directly in your code than you'd have be calling an external
    function. Your code is a clear demonstration why "get_real_size" is
    /not/ a function that is needed at all.

    And in low-level library interfaces like this, an unneeded function is a liability - it becomes an interface that must be maintained even if it
    is no longer relevant, and can hinder implementation changes later. You
    don't put functions or functionality in an interface unless they add
    real, useful value. This one does not. (Nor does James' "resize"
    suggestion, distinct from his "realloc" function.)


    --- Synchronet 3.19c-Linux NewsLink 1.113
  • From Bart@bc@freeuk.com to comp.lang.misc on Sun Nov 27 18:18:49 2022
    From Newsgroup: comp.lang.misc

    On 27/11/2022 17:11, David Brown wrote:
    On 27/11/2022 16:47, Bart wrote:
    On 27/11/2022 15:18, David Brown wrote:

    Trying to optimise your interface around pathological examples is
    madness.  Design the interface to be useful for real, practical code.

    So why do you think realloc is so slow in this context, compared with
    how realloc was called in my other post? (Repeated below for
    convenience.)

    That is, the above dumb use of realloc shows O(n^2) behaviour compared
    with the version below which is O(n). Because you are saying above
    that it should be just as efficient.


    No - I am saying that using some kind of "get_real_size" gives you nothing.

    Only an idiot would call realloc() or anything remotely like that a byte
    at a time.  (We are talking about low-level languages here - in a high level language, you might append to a string or list one element at a
    time, expecting the language to implement things efficiently.)

    People do have memory blocks that can grow by small amounts, or a byte
    at a time; how do you think such growth should be implemented, by
    millions of calls to realloc for a byte-at-a-time reallocation, or
    something a bit cleverer?

    It's that 'a bit cleverer' part that was the purpose of
    'get_real_size', except I would go further.

    The code that tracks capacity is vastly cleverer than code that
    allocates a byte at a time.  "get_real_size" would add /nothing/ to it, because you already track the capacity - and it is more efficient to do
    so directly in your code than you'd have be calling an external
    function.  Your code is a clear demonstration why "get_real_size" is
    /not/ a function that is needed at all.

    I use my own memory allocator functions, notably from within my
    interpreters.

    The interpreter creates object descriptors for flexible data that
    includes 'capacity'. So you're right, it's done within the app.

    But where does it get that capacity figure from? It's from my memory allocation functions. (There, the actual allocated size is returned in a global that the caller of `pcm_alloc` can pick up; not a special function.)

    That way the application doesn't itself need to get involved in
    allocation strategies or free lists and so on.

    That value that is provided is equivalent to that hypothetical
    'get_real_size` function.

    One difference however is that, being my library, I know the spare
    capacities for allocated blocks are generous. So with a third party
    library, I would need to know or control that aspect of it.

    If not generous enough, I would need to build-in extra capacities within
    my application, but even then, I would like to make use of any extra
    allocated space there happens to be, and that needs 'get_real_size' or equivalent.

    And, yes, the language implemented with my interpreter can very
    efficiently, for interpreted code, grow very large strings a character
    at time, or grow any list or array the same way. In my programs that's a common idiom.

    --- Synchronet 3.19c-Linux NewsLink 1.113
  • From David Brown@david.brown@hesbynett.no to comp.lang.misc on Mon Nov 28 08:11:44 2022
    From Newsgroup: comp.lang.misc

    On 26/11/2022 14:05, James Harris wrote:
    On Friday, 25 November 2022 at 16:15:08 UTC, David Brown wrote:
    On 25/11/2022 14:02, James Harris wrote:
    I will get back to you guys on other topics but I need to improve
    the memory allocator I use in my compiler and that has led to the
    following query.

    Imagine an allocator which carries out plain malloc-type
    allocations (i.e. specified as a number of 8-bit bytes suitably
    aligned for any type) without having to have compatibility with
    malloc, and in a language which is not C.

    For the sake of discussion the set of calls could be

    m_alloc m_calloc m_realloc m_resize m_free
    In a new language, I would not follow the names from C as closely.
    For one thing, it could confuse people - they may think they do the
    same thing as in C. For another, "calloc" in particular is a silly
    name. And you don't need "realloc" and "resize".

    The idea is that realloc could move the allocation whereas resize
    could only resize it in place. The latter is not present in C's
    malloc family.


    Where would such a function be useful? If the application needs to
    expand an object, it needs to expand it - if that means moving it, so be
    it. I can't see where you would have use for a function that /might/ be
    able to expand an object. Do you have /real/ use-cases in mind? (I
    don't mean examples like Bart's attempts at making the silliest code he
    can think of.)



    It can make sense to distinguish between "alloc_zeroed" and
    "alloc_unintialised", for performance reasons.

    Agreed, but what exactly would you want for the calls? Maybe

    m_alloc(size) m_alloc_a(size, flags)

    ?


    There could be many different interfaces. Dmitry also pointed out other possible distinctions for allocation of shared memory, unpageable
    memory, and so on. Not all memory is created equal!

    Maybe you want a more object-oriented interface, for supporting several different pools or memory allocation choices.

    I don't know if you plan to support overloaded functions, so that you
    can use the same name with different parameter numbers and types. That
    works well with tag types.

    The one thing I would avoid at all costs, in any interfaces, is the
    "integer flag" nonsense found in many languages (such as C). If you
    really want some kind of flags, then at least be sure your language has
    strong type-checked enumerations.



    Would there be any value in giving the programmer a way to find
    out the size of a given allocation? I don't mean the size
    requested but the size allocated (which would be greater than or
    equal to the size requested).

    No. It is pointless.

    Why would anyone want to know the result of such a function? The
    only conceivable thought would be if they had first allocated space
    for x units, and then they now want to store y units - calling
    "get_real_size" could let them know if they need to call "resize"
    or not.

    The answer is that they should simply call "resize". If "resize"
    does not need to allocate new memory because the real size is big
    enough, it does nothing.

    It's partly for performance, as Bart's comments have backed up. If
    code can find out the capacity then it can fill that allocation up to
    the stated capacity without having to make any other calls and
    without risking moving what has been stored so far.


    There is no performance benefit in real code - and you should not care
    about silly cases.

    I ask because it's simple enough to precede an aligned chunk of
    memory with the allocation size. The allocator may well do that
    anyway in order to help it manage memory. So there seems to be no
    good reason to keep that info from the programmer. The question
    is over whether there's value in allowing the programmer to get
    such info. I am thinking that he could get the value with a call
    such as

    m_msize(p)

    It seems simple enough, potentially useful, and should be
    harmless. But it's noticeable that the malloc-type calls don't
    have anything similar. So maybe there's a good reason why not.

    I thought it might cause implementation issues such as requiring
    a larger header if the allocator wants to search only free
    memory. But whatever implementation is used I think the allocator
    will still need to either store or calculate the size of the
    memory allocated so I am not sure I see a problem with the idea.

    What do you guys think? Opinions welcome!

    I think you should not try to copy C's malloc/free mechanism. In
    particular, I do not think the memory allocator should track the
    sizes of allocations. "free" should always include the size,
    matching the value used in "alloc". (And "resize" should pass the
    old size as well as the new size.)

    Bart said the same and the suggestion surprises me. It seems prone to
    error.


    Your choices with memory management are basically to either trust the programmer, or do everything automatically with garbage collection.
    Even if you use something like C++'s RAII mechanisms for allocating and deallocating, you still have to trust the programmer to some extent. If
    you are relying on the programmer to get their pointers right for calls
    to "free", why are you worried that they'll get the size wrong?


    Storing the size of allocation was not too bad in earliest days of
    C, with simpler processors, no caches, no multi-threading, and
    greater concern for simple allocation implementations than for
    performance or reliability. Modern allocators no longer work with a
    simple linked list storing sizes and link pointers at an address
    below the address returned to the user, so why follow a similar
    interface?

    Programs know the size of memory they requested when they allocated
    it. They know, almost invariably, the size when they are freeing
    the memory. They know the size of the types, and the size of the
    arrays. So having the memory allocator store this too is a waste.

    That's sometimes the case but not always. Say I wanted to read a line
    from a file a byte at a time without knowing in advance how long the
    line would be (a common-enough requirement but one which C programs
    all too often fudge by defining a maximum line length). The required allocation could not be known in advance.


    That is irrelevant. (On a side note, it makes sense to have library
    calls that make this kind of common function more convenient.) The
    programmer knows the size of any "malloc" calls or "realloc" calls made
    - that's the size given back to "free".

    There are various specialised allocation techniques but the malloc
    style is the most general I know of.


    No, it is not particularly general - it's just relatively simple. And
    it is a piss-poor interface for a modern language where you should be
    thinking about things like thread-safety, cache friendliness and user convenience, not picking your interface based on a throw-away example
    from an old C book (which is where malloc/free comes from - it was a
    simple example, not intended as a real memory allocation implementation).

    As Dmitry has pointed out, there are /many/ ways to make allocators, and
    none of them are optimal in all cases. Trying to squeeze things into
    one very limited interface simply guarantees that you never get the best choice in your code.


    Once you've dropped size tracking, you can easily separate
    allocation tracking from the memory in use - this gives neater
    sizes for pools and better cache and virtual page usage. You can
    have pools of different sizes - perhaps 16 byte blocks for the
    smallest, then 256 byte blocks, then 4 KB blocks, etc. (You might
    prefer different sizes, perhaps with more intermediary sizes.) A
    pool element would have 64 blocks. Allocation within a pool is done
    by quick searches within a single 64-bit map - no need to search
    through lists.

    I presume by pools you mean slots of a certain size I may, longer
    term, let a program specify slot sizes and pool geometry. But that's
    for the future and it wouldn't work well with variable-length
    strings, especially where the length is not known in advance. So I
    need something simple and general for now.


    Make a bad interface now and you'll be stuck with it.

    I still don't understand your targets - either in terms of computer
    systems, or expected uses. If the target is modern PC-style systems (so
    that you can, like in Bart's languages, assume that you have 64-bit
    integers and plenty of memory), then you can make a string format that
    has uses, say, 256 byte lumps. Some of that is for pointers and length
    counts to handle chains for big strings, and maybe reference counting,
    with over 200 bytes free for the string itself. The solid majority of
    all strings in practical use will fit in one lump, and you have no
    challenges with memory allocators, resizing, or the rest of it. That simplification will completely outweigh the inefficiency of the wasted
    memory. And then for long strings, doing everything in a single C-style
    block is inefficient anyway - you'll want some kind of linked chain anyway.



    (Of course real implementations involve a fair bit of complexity
    and care, especially for thread safety. But the emphasis must be on
    cache friendliness.)

    Agreed.



    --- Synchronet 3.19c-Linux NewsLink 1.113
  • From David Brown@david.brown@hesbynett.no to comp.lang.misc on Mon Nov 28 11:17:26 2022
    From Newsgroup: comp.lang.misc

    On 27/11/2022 19:18, Bart wrote:
    On 27/11/2022 17:11, David Brown wrote:
    On 27/11/2022 16:47, Bart wrote:
    On 27/11/2022 15:18, David Brown wrote:

    Trying to optimise your interface around pathological examples is
    madness.  Design the interface to be useful for real, practical code.

    So why do you think realloc is so slow in this context, compared with
    how realloc was called in my other post? (Repeated below for
    convenience.)

    That is, the above dumb use of realloc shows O(n^2) behaviour
    compared with the version below which is O(n). Because you are saying
    above that it should be just as efficient.


    No - I am saying that using some kind of "get_real_size" gives you
    nothing.

    Only an idiot would call realloc() or anything remotely like that a
    byte at a time.  (We are talking about low-level languages here - in a
    high level language, you might append to a string or list one element
    at a time, expecting the language to implement things efficiently.)

    People do have memory blocks that can grow by small amounts, or a
    byte at a time; how do you think such growth should be implemented,
    by millions of calls to realloc for a byte-at-a-time reallocation, or
    something a bit cleverer?

    It's that 'a bit cleverer' part that was the purpose of
    'get_real_size', except I would go further.

    The code that tracks capacity is vastly cleverer than code that
    allocates a byte at a time.  "get_real_size" would add /nothing/ to
    it, because you already track the capacity - and it is more efficient
    to do so directly in your code than you'd have be calling an external
    function.  Your code is a clear demonstration why "get_real_size" is
    /not/ a function that is needed at all.

    I use my own memory allocator functions, notably from within my interpreters.

    The interpreter creates object descriptors for flexible data that
    includes 'capacity'. So you're right, it's done within the app.

    But where does it get that capacity figure from? It's from my memory allocation functions. (There, the actual allocated size is returned in a global that the caller of `pcm_alloc` can pick up; not a special function.)


    The normal way is for the code to ask the allocator "can I have 1000
    bytes?", and when the allocator says "yes, here's a pointer to it" you
    say "great - I have a capacity of 1000 bytes".

    It seems completely wrong to me for the code then to ask the allocator
    "You know I asked for 1000 bytes? What did you /really/ give me?". If
    I needed more than 1000 bytes, I'd have asked for more than 1000 bytes.
    If I /later/ need more than 1000 bytes, I'll ask for that /later/.

    So later on, when I need 1020 bytes instead of 1000, I'll ask "You know
    that 1000 byte block you gave me? Can you fiddle things and make it
    1020?". I don't mind if the allocator does this by noting that there is already 1020 space and returning quickly, or by doing a new allocation -
    I need these 1020 bytes, or I would not have asked for them.

    I cannot see any point in first asking how much space I /really/ have,
    before asking for the 1020 bytes. What is the point?

    The one thing I would /never/ do is ask originally for 1000 bytes, then
    ask how much I got, and then use those extra bytes. That would be lying
    to the allocator. When people start doing that kind of thing, it means
    they are making undocumented assumptions about implementations - and
    that means implementations get stuck with with these limitations, or
    user code breaks.


    That way the application doesn't itself need to get involved in
    allocation strategies or free lists and so on.

    That value that is provided is equivalent to that hypothetical 'get_real_size` function.


    No, that is not needed in any way. Ask for what you need from the
    allocator - that's all.

    One difference however is that, being my library, I know the spare capacities for allocated blocks are generous. So with a third party
    library, I would need to know or control that aspect of it.

    If not generous enough, I would need to build-in extra capacities within
    my application, but even then, I would like to make use of any extra allocated space there happens to be, and that needs 'get_real_size' or equivalent.

    And, yes, the language implemented with my interpreter can very
    efficiently, for interpreted code, grow very large strings a character
    at time, or grow any list or array the same way. In my programs that's a common idiom.


    That is fine in a high-level language. And it is the /language/ and its run-time that must handle this efficiently. Typically that will be done
    by something similar to the code you wrote, allocating in chunks rather
    than byte by byte. This run-time sits between the application code
    (where the usage should be as convenient as possible) and the low-level allocation code (which should be efficient).
    --- Synchronet 3.19c-Linux NewsLink 1.113
  • From Dmitry A. Kazakov@mailbox@dmitry-kazakov.de to comp.lang.misc on Mon Nov 28 12:34:20 2022
    From Newsgroup: comp.lang.misc

    On 2022-11-28 11:17, David Brown wrote:
    I cannot see any point in first asking how much space I /really/ have, before asking for the 1020 bytes.  What is the point?

    No point, you are right.

    However pool hooking facilities usually provide that information. E.g.
    you could have an allocation callback with information like actual
    address (rather than aligned pointer), actual size, next used block etc.
    For example, Linux has an incredibly poorly designed malloc_hook:

    https://www.man7.org/linux/man-pages/man3/malloc_hook.3.html

    In any case, programmers with special needs must have an ability to
    implement their own pools like arenas, mark and release pools etc.
    --
    Regards,
    Dmitry A. Kazakov
    http://www.dmitry-kazakov.de

    --- Synchronet 3.19c-Linux NewsLink 1.113
  • From Bart@bc@freeuk.com to comp.lang.misc on Mon Nov 28 14:04:28 2022
    From Newsgroup: comp.lang.misc

    On 28/11/2022 10:17, David Brown wrote:
    On 27/11/2022 19:18, Bart wrote:

    But where does it get that capacity figure from? It's from my memory
    allocation functions. (There, the actual allocated size is returned in
    a global that the caller of `pcm_alloc` can pick up; not a special
    function.)


    The normal way is for the code to ask the allocator "can I have 1000 bytes?", and when the allocator says "yes, here's a pointer to it" you
    say "great - I have a capacity of 1000 bytes".

    It seems completely wrong to me for the code then to ask the allocator
    "You know I asked for 1000 bytes?  What did you /really/ give me?".  If
    I needed more than 1000 bytes, I'd have asked for more than 1000 bytes.
     If I /later/ need more than 1000 bytes, I'll ask for that /later/.

    So later on, when I need 1020 bytes instead of 1000, I'll ask "You know
    that 1000 byte block you gave me?  Can you fiddle things and make it 1020?".  I don't mind if the allocator does this by noting that there is already 1020 space and returning quickly, or by doing a new allocation -
    I need these 1020 bytes, or I would not have asked for them.

    I cannot see any point in first asking how much space I /really/ have, before asking for the 1020 bytes.  What is the point?

    You're never curious about how much spare capacity was actually set
    aside for your request? You don't think there is any way to profitably
    exploit that?

    You don't want to know how much memory is being wasted on your behalf?

    It looks like you want to assume the allocator will set aside zero spare capacity, and you want to offload ALL the work of efficiently allocating gradually expanding buffers onto the application.

    Remember the subject is about /creating/ the allocation library. That
    means being able to specify how it will work. I would rather some of
    that work was offloaded to the allocator, since the author of the
    library may have a better idea of allocation strategies than I do.

    The one thing I would /never/ do is ask originally for 1000 bytes, then
    ask how much I got, and then use those extra bytes.

    You use those extra bytes when it transpires you need more.

      That would be lying
    to the allocator.  When people start doing that kind of thing, it means they are making undocumented assumptions about implementations - and
    that means implementations get stuck with with these limitations, or
    user code breaks.

    That sounds backwards. In what way is it to making assumptions to
    actually ASK what an implementation has reserved?

    If you work on the basis that there are zero extra bytes, isn't THAT
    making an assumption?

    And, yes, the language implemented with my interpreter can very
    efficiently, for interpreted code, grow very large strings a character
    at time, or grow any list or array the same way. In my programs that's
    a common idiom.


    That is fine in a high-level language.  And it is the /language/ and its run-time that must handle this efficiently.  Typically that will be done
    by something similar to the code you wrote, allocating in chunks rather
    than byte by byte.

    No, byte-by-byte or int-by-int or record-by-record is what the
    user-program requests. Chunk-by-chunk is what the implementation has to
    do to get it efficient.


    --- Synchronet 3.19c-Linux NewsLink 1.113
  • From Dmitry A. Kazakov@mailbox@dmitry-kazakov.de to comp.lang.misc on Mon Nov 28 16:03:50 2022
    From Newsgroup: comp.lang.misc

    On 2022-11-28 15:04, Bart wrote:

    You don't want to know how much memory is being wasted on your behalf?

    https://man7.org/linux/man-pages/man3/mallinfo.3.html

    It looks like you want to assume the allocator will set aside zero spare capacity,

    That is a reasonable assumption. Note that frequently dynamically
    allocated library data types are designed to work with all possible implementations of pools, e.g. a pool is taken as a parameter of a
    generic or OO instance. Therefore the weakest assumption is made.
    --
    Regards,
    Dmitry A. Kazakov
    http://www.dmitry-kazakov.de

    --- Synchronet 3.19c-Linux NewsLink 1.113
  • From Bart@bc@freeuk.com to comp.lang.misc on Mon Nov 28 16:48:55 2022
    From Newsgroup: comp.lang.misc

    On 28/11/2022 15:03, Dmitry A. Kazakov wrote:
    On 2022-11-28 15:04, Bart wrote:

    You don't want to know how much memory is being wasted on your behalf?

    https://man7.org/linux/man-pages/man3/mallinfo.3.html

    It looks like you want to assume the allocator will set aside zero
    spare capacity,

    That doesn't look to useful. It's far more complex than someone would
    want, and is not specific to an individual allocation.


    That is a reasonable assumption. Note that frequently dynamically
    allocated library data types are designed to work with all possible implementations of pools, e.g. a pool is taken as a parameter of a
    generic or OO instance. Therefore the weakest assumption is made.


    But also unlikely. Someone one requests 37 bytes; will the allocator
    return a pointer to exactly 37 bytes from the pool, so that the next
    request is 37 bytes offset from the last?

    This would make that next allocation misaligned.

    Calls to MS' VirtualAlloc are rounded up to a multiple of 4KB for fresh allocations.

    Calls to my malloc() just appear to do basic rounding, although this
    also has to accommodate size info, but two successive calls to malloc(1) produces addresses that differ by +32 bytes.

    Calls my own routines, implemented on top of malloc(), round to powers
    of two for sizes of 16 bytes to 33MB (this was created with my
    interpreter in mind).

    Three very different behaviours. So it makes sense not to assume, but to
    ask.

    --- Synchronet 3.19c-Linux NewsLink 1.113
  • From David Brown@david.brown@hesbynett.no to comp.lang.misc on Mon Nov 28 17:50:36 2022
    From Newsgroup: comp.lang.misc

    On 28/11/2022 16:03, Dmitry A. Kazakov wrote:
    On 2022-11-28 15:04, Bart wrote:

    You don't want to know how much memory is being wasted on your behalf?

    https://man7.org/linux/man-pages/man3/mallinfo.3.html

    It looks like you want to assume the allocator will set aside zero
    spare capacity,

    That is a reasonable assumption. Note that frequently dynamically
    allocated library data types are designed to work with all possible implementations of pools, e.g. a pool is taken as a parameter of a
    generic or OO instance. Therefore the weakest assumption is made.


    No, the weakest assumption (which is the one made by most programmers)
    is that you do not know anything about how much wasted or spare capacity
    there might be - you assume only that the allocation function does what
    it says it will do. So when you ask for 100 bytes, you assume nothing
    more than that you have 100 bytes. You don't assume you might have
    spare memory, or wasted memory - but you also do not assume that there
    is zero extra or wasted memory. You assume nothing at all, and rely
    solely on the specification of the allocation functions.


    --- Synchronet 3.19c-Linux NewsLink 1.113
  • From David Brown@david.brown@hesbynett.no to comp.lang.misc on Mon Nov 28 17:50:38 2022
    From Newsgroup: comp.lang.misc

    On 28/11/2022 15:04, Bart wrote:
    On 28/11/2022 10:17, David Brown wrote:
    On 27/11/2022 19:18, Bart wrote:

    But where does it get that capacity figure from? It's from my memory
    allocation functions. (There, the actual allocated size is returned
    in a global that the caller of `pcm_alloc` can pick up; not a special
    function.)


    The normal way is for the code to ask the allocator "can I have 1000
    bytes?", and when the allocator says "yes, here's a pointer to it" you
    say "great - I have a capacity of 1000 bytes".

    It seems completely wrong to me for the code then to ask the allocator
    "You know I asked for 1000 bytes?  What did you /really/ give me?".
    If I needed more than 1000 bytes, I'd have asked for more than 1000
    bytes.   If I /later/ need more than 1000 bytes, I'll ask for that
    /later/.

    So later on, when I need 1020 bytes instead of 1000, I'll ask "You
    know that 1000 byte block you gave me?  Can you fiddle things and make
    it 1020?".  I don't mind if the allocator does this by noting that
    there is already 1020 space and returning quickly, or by doing a new
    allocation - I need these 1020 bytes, or I would not have asked for them.

    I cannot see any point in first asking how much space I /really/ have,
    before asking for the 1020 bytes.  What is the point?

    You're never curious about how much spare capacity was actually set
    aside for your request? You don't think there is any way to profitably exploit that?


    I haven't been particularly curious about that, no. And no, I really do
    not think there is any way to make use of the information - the benefits would, at most, be absolutely miniscule.

    There are, of course, times when you really want to count each byte and
    track things in detail - I do that regularly in my line of work. But
    the answer there is to avoid dynamic memory as much as you possibly can,
    and often to use specific memory allocation routines designed for the particular task in hand. In cases where I might use malloc and free
    from the standard library, I really don't care how many extra bytes are allocated.


    You don't want to know how much memory is being wasted on your behalf?


    When I need to know, I know - and not through a pointless
    "get_real_size" function, but by making sure I have an appropriate
    memory allocation system for the job. When I have a big system - say,
    an embedded Linux board with 2 GB of memory - why would I care if an allocation is wasting 8 bytes or 16 bytes? How could it possibly make a difference in real life? You need to be doing /millions/ of allocations
    to have a waste that is possibly worth measuring. (And if you are doing millions of allocations, you won't be doing it with malloc - you'll be
    using a specialised allocator optimised for the use-case.)

    It looks like you want to assume the allocator will set aside zero spare capacity, and you want to offload ALL the work of efficiently allocating gradually expanding buffers onto the application.


    No, I did not say that.

    If you are writing low-level code with a low-level language, then you
    will usually want to do that - only the application programmer knows the patterns he/she will need, and can pick the ideal sizes for buffers and increases in allocation size. (Do you want to double each realloc? Add
    100 bytes each time? The allocator doesn't know.) As you have shown in sample code, tracking the capacity in application code is not hard.

    If you are writing high-level code in a high-level language, the
    language run-time libraries will handle these details using heuristics
    to give an okay result in a wide range of cases.

    As for how much extra space gets allocated by a standard library
    allocator, I do not care (other than assuming it is not an unreasonable
    waste - I assume that people writing standard libraries for a given
    platform write appropriate code for that platform).

    Seriously - if you want 1000 bytes, you ask malloc for 1000 bytes. You
    don't ask for 990 bytes and assume it will give you at least 10 bytes
    extra. You ask for what you need.


    Remember the subject is about /creating/ the allocation library. That
    means being able to specify how it will work. I would rather some of
    that work was offloaded to the allocator, since the author of the
    library may have a better idea of allocation strategies than I do.


    Did you miss the bit where I explained, several times, about how the implementation of "realloc" could use the extra space if it happens to
    be enough to satisfy the new request? In such cases, realloc will be extremely fast.

    The one thing I would /never/ do is ask originally for 1000 bytes,
    then ask how much I got, and then use those extra bytes.

    You use those extra bytes when it transpires you need more.


    No, when you need more, you ask for more. If there are enough extra
    bytes, "realloc" will return immediately. You don't need user code to
    faff around with "get_real_size" - it's just more work for the
    programmer, more opportunities for errors, probably slower, and more
    limiting for implementation of the allocation system.

      That would be lying
    to the allocator.  When people start doing that kind of thing, it
    means they are making undocumented assumptions about implementations -
    and that means implementations get stuck with with these limitations,
    or user code breaks.

    That sounds backwards. In what way is it to making assumptions to
    actually ASK what an implementation has reserved?


    You are assuming particular characteristics of the allocator that are inappropriate for a general-purpose memory allocator. All a low-level
    general allocator needs to do is have a function to allocate memory, a function to release memory, and optionally (but usefully) a function to increase an allocation efficiently. Every function you include beyond
    that, every detail in specifications, is a limitation on the flexibility
    of the allocator.

    Here's an example.

    Suppose I make an allocator that allocates everything in 128-byte
    blocks, tracked with bitmaps. You ask for 800 bytes - I give you that
    by allocating 7 blocks, for a total of 896 bytes. You now want to
    increase the size of the allocation, ideally to 1000 bytes. You call
    the "get_real_size" function and find there are 96 bytes extra. What is
    your code going to do now? Maybe it thinks getting more than 896 bytes
    is going to be slow, and makes do with just 896 bytes. But that could
    be wrong - perhaps my allocator happens to have the next block free, so increasing to 1000 bytes would be almost instant - just a check and an
    OR on a bitmap. Maybe it thinks it really needs the 1000 bytes, in
    which case the call to "get_real_size" is completely wasted. The
    information from "get_real_size" is /useless/. It gives you nothing.

    But what is the cost to /me/, the implementer of the allocation system?
    If I have made a "get_real_size" function, and let you use it to get
    extra space, I have to stand by that - if I've told you that you really
    got 896 bytes, then I can't re-use the extra 96 bytes in any way.
    However, if I /don't/ give you such a function, then those 96 bytes are
    still mine. I can make version 2 of the library, with a sub-block
    allocator so that when you ask for 10 bytes, you get it from there. The
    very act of providing a "get_real_size" function guarantees that those
    96 bytes are wasted and cannot be used.

    If you work on the basis that there are zero extra bytes, isn't THAT
    making an assumption?


    No one is assuming that there are zero extra bytes.

    And, yes, the language implemented with my interpreter can very
    efficiently, for interpreted code, grow very large strings a
    character at time, or grow any list or array the same way. In my
    programs that's a common idiom.


    That is fine in a high-level language.  And it is the /language/ and
    its run-time that must handle this efficiently.  Typically that will
    be done by something similar to the code you wrote, allocating in
    chunks rather than byte by byte.

    No, byte-by-byte or int-by-int or record-by-record is what the
    user-program requests. Chunk-by-chunk is what the implementation has to
    do to get it efficient.


    No, that is not what user programs request for explicit low-level memory allocation. Programs allocate in lumps if they can, at least for memory buffers that might be realloc'ed (which is what we are talking about here).


    --- Synchronet 3.19c-Linux NewsLink 1.113
  • From Dmitry A. Kazakov@mailbox@dmitry-kazakov.de to comp.lang.misc on Mon Nov 28 17:56:03 2022
    From Newsgroup: comp.lang.misc

    On 2022-11-28 17:50, David Brown wrote:
    On 28/11/2022 16:03, Dmitry A. Kazakov wrote:
    On 2022-11-28 15:04, Bart wrote:

    You don't want to know how much memory is being wasted on your behalf?

    https://man7.org/linux/man-pages/man3/mallinfo.3.html

    It looks like you want to assume the allocator will set aside zero
    spare capacity,

    That is a reasonable assumption. Note that frequently dynamically
    allocated library data types are designed to work with all possible
    implementations of pools, e.g. a pool is taken as a parameter of a
    generic or OO instance. Therefore the weakest assumption is made.

    No, the weakest assumption (which is the one made by most programmers)
    is that you do not know anything about how much wasted or spare capacity there might be - you assume only that the allocation function does what
    it says it will do.

    Yes, this is what I meant. One cannot rely on any internal heuristics.

      So when you ask for 100 bytes, you assume nothing
    more than that you have 100 bytes.  You don't assume you might have
    spare memory, or wasted memory - but you also do not assume that there
    is zero extra or wasted memory.  You assume nothing at all, and rely
    solely on the specification of the allocation functions.

    Yes, that is weakest precondition: only the requested space was allocated.
    --
    Regards,
    Dmitry A. Kazakov
    http://www.dmitry-kazakov.de

    --- Synchronet 3.19c-Linux NewsLink 1.113
  • From Dmitry A. Kazakov@mailbox@dmitry-kazakov.de to comp.lang.misc on Mon Nov 28 18:25:05 2022
    From Newsgroup: comp.lang.misc

    On 2022-11-28 17:48, Bart wrote:
    On 28/11/2022 15:03, Dmitry A. Kazakov wrote:
    On 2022-11-28 15:04, Bart wrote:

    You don't want to know how much memory is being wasted on your behalf?

    https://man7.org/linux/man-pages/man3/mallinfo.3.html

    It looks like you want to assume the allocator will set aside zero
    spare capacity,

    That doesn't look to useful. It's far more complex than someone would
    want, and is not specific to an individual allocation.


    That is a reasonable assumption. Note that frequently dynamically
    allocated library data types are designed to work with all possible
    implementations of pools, e.g. a pool is taken as a parameter of a
    generic or OO instance. Therefore the weakest assumption is made.


    But also unlikely. Someone one requests 37 bytes; will the allocator
    return a pointer to exactly 37 bytes from the pool, so that the next
    request is 37 bytes offset from the last?

    Could be.

    This would make that next allocation misaligned.

    If 1 was the alignment requested. The minimal interface of Allocate is

    - Size in storage elements
    - Alignment

    Note that pools are implemented in a huge variety of ways.

    Calls to MS' VirtualAlloc are rounded up to a multiple of 4KB for fresh allocations.

    VirtualAlloc is not a memory pool allocator. It is a part of paged
    memory API.
    --
    Regards,
    Dmitry A. Kazakov
    http://www.dmitry-kazakov.de

    --- Synchronet 3.19c-Linux NewsLink 1.113
  • From Bart@bc@freeuk.com to comp.lang.misc on Mon Nov 28 17:54:14 2022
    From Newsgroup: comp.lang.misc

    On 28/11/2022 16:50, David Brown wrote:
    On 28/11/2022 15:04, Bart wrote:

    You don't want to know how much memory is being wasted on your behalf?


    When I need to know, I know - and not through a pointless
    "get_real_size" function, but by making sure I have an appropriate
    memory allocation system for the job.  When I have a big system - say,
    an embedded Linux board with 2 GB of memory - why would I care if an allocation is wasting 8 bytes or 16 bytes?  How could it possibly make a difference in real life?  You need to be doing /millions/ of allocations
    to have a waste that is possibly worth measuring.  (And if you are doing millions of allocations, you won't be doing it with malloc - you'll be
    using a specialised allocator optimised for the use-case.)

    My malloc() seems to use a minimum of 32 bytes per allocation. And yes
    people /do/ use malloc() without considering possible inefficiencies.

    (One of my compilers uses my own allocator library. If I switch to using malloc() directly, compilation speed is 80% slower.

    And, on the same test, memory usage seems to be 20% higher (0.6GB vs
    0.5GB, but I can't measure accurately, because, well, there is no way to
    find out exactly how much malloc is using.)


    Remember the subject is about /creating/ the allocation library. That
    means being able to specify how it will work. I would rather some of
    that work was offloaded to the allocator, since the author of the
    library may have a better idea of allocation strategies than I do.


    Did you miss the bit where I explained, several times, about how the implementation of "realloc" could use the extra space if it happens to
    be enough to satisfy the new request?  In such cases, realloc will be extremely fast.

    I'm sorry but I don't trust it to be fast. See how much longer malloc()
    took on my test above; nearly 50% of all runtime was spent inside
    malloc(); why is it so much slower?


    That sounds backwards. In what way is it to making assumptions to
    actually ASK what an implementation has reserved?

    Suppose I make an allocator that allocates everything in 128-byte
    blocks, tracked with bitmaps.  You ask for 800 bytes - I give you that
    by allocating 7 blocks, for a total of 896 bytes.  You now want to
    increase the size of the allocation, ideally to 1000 bytes.  You call
    the "get_real_size" function and find there are 96 bytes extra.  What is your code going to do now?  Maybe it thinks getting more than 896 bytes
    is going to be slow, and makes do with just 896 bytes.  But that could
    be wrong - perhaps my allocator happens to have the next block free, so increasing to 1000 bytes would be almost instant - just a check and an
    OR on a bitmap.  Maybe it thinks it really needs the 1000 bytes, in
    which case the call to "get_real_size" is completely wasted.  The information from "get_real_size" is /useless/.  It gives you nothing.

    But what is the cost to /me/, the implementer of the allocation system?
    If I have made a "get_real_size" function, and let you use it to get
    extra space, I have to stand by that - if I've told you that you really
    got 896 bytes, then I can't re-use the extra 96 bytes in any way.
    However, if I /don't/ give you such a function, then those 96 bytes are still mine.  I can make version 2 of the library, with a sub-block allocator so that when you ask for 10 bytes, you get it from there.  The very act of providing a "get_real_size" function guarantees that those
    96 bytes are wasted and cannot be used.

    It seems simple to me. If the allocator plans on using any reserved
    space, then get_real-size returns the exact requested, no extra.

    If it includes any extra on version 1, then on version 2 the behaviour
    of get_real_size can change to exclude it.

    The fact is that MY allocator DOES have the equivalent of
    `get_real_size`. (However, because it does not record block sizes, the
    info is put into a global - it could have been a function - that the
    caller must make use of before the next allocation. Or I can change the
    API so that the allocator returns a second pieces of information.)

    This works extremely well.

    So why is it that it can't work on any of the allocators you use? Or any
    that someone else might create? Maybe I should rewrite mine because
    according to use, it cannot work and cannot give any benefits!

    I think perhaps you're getting confused about what goes into the
    allocation library, what goes into the language implementation or
    runtime, and what goes into the user's application.


    No, byte-by-byte or int-by-int or record-by-record is what the
    user-program requests. Chunk-by-chunk is what the implementation has
    to do to get it efficient.


    No, that is not what user programs request for explicit low-level memory allocation.  Programs allocate in lumps if they can, at least for memory buffers that might be realloc'ed (which is what we are talking about here).


    It depends on the language. One that performs the allocations for you
    (which is most of them now), especially allocations that can grow, will
    have user-programs write to an array element at a time, and the
    implemention, if it allows that, will need to do so efficiently.

    And that means not leaving it 100% to the allocation library.




    --- Synchronet 3.19c-Linux NewsLink 1.113
  • From Bart@bc@freeuk.com to comp.lang.misc on Mon Nov 28 17:57:04 2022
    From Newsgroup: comp.lang.misc

    On 28/11/2022 17:25, Dmitry A. Kazakov wrote:
    On 2022-11-28 17:48, Bart wrote:


    This would make that next allocation misaligned.

    If 1 was the alignment requested. The minimal interface of Allocate is

    - Size in storage elements
    - Alignment

    Note that pools are implemented in a huge variety of ways.

    Returning pointers which are not alligned to at least 64 bits would be
    very poor, if there is no way to control that.

    Calls to MS' VirtualAlloc are rounded up to a multiple of 4KB for
    fresh allocations.

    VirtualAlloc is not a memory pool allocator. It is a part of paged
    memory API.


    I don't care how it works. I want N bytes, it will return a pointer N
    bytes, just like any other allocator. (I have to use this one when I
    need executable memory.)
    --- Synchronet 3.19c-Linux NewsLink 1.113
  • From David Brown@david.brown@hesbynett.no to comp.lang.misc on Mon Nov 28 20:01:26 2022
    From Newsgroup: comp.lang.misc

    On 28/11/2022 18:54, Bart wrote:
    On 28/11/2022 16:50, David Brown wrote:
    On 28/11/2022 15:04, Bart wrote:

    You don't want to know how much memory is being wasted on your behalf?


    When I need to know, I know - and not through a pointless
    "get_real_size" function, but by making sure I have an appropriate
    memory allocation system for the job.  When I have a big system - say,
    an embedded Linux board with 2 GB of memory - why would I care if an
    allocation is wasting 8 bytes or 16 bytes?  How could it possibly make
    a difference in real life?  You need to be doing /millions/ of
    allocations to have a waste that is possibly worth measuring.  (And if
    you are doing millions of allocations, you won't be doing it with
    malloc - you'll be using a specialised allocator optimised for the
    use-case.)

    My malloc() seems to use a minimum of 32 bytes per allocation. And yes people /do/ use malloc() without considering possible inefficiencies.

    Of course they do. People use all sorts of things in their programming without considering possible inefficiencies - this is known as "using
    the right level of abstraction". You always have a balance between
    maximum control to get the most efficient results (assuming - usually incorrectly - that the programmer knows enough to get optimal results),
    or relinquishing control for easier and faster development. Do you
    think people should always worry about efficiency before every call to "printf"? Or concern themselves about how their loop structures will
    interact with the cpu's branch prediction? Of course not. /Some/
    people, for certain code, need to worry about that kind of thing. But
    for most programmers and most situations, the correct choice is to use
    the simplest source code and not worry.

    The same applies to memory allocation. If you are worrying about a
    possible inefficiency of 32 bytes in a memory allocation, you are doing
    things /wrong/.

    (Note that implementing a language interpreter or run-time library is
    one of these few times when you might want to concern yourself a little
    about such efficiencies - but still not /too/ much. And we are
    discussing the use of these facilities in user code, rather than the
    actual implementations.)


    (One of my compilers uses my own allocator library. If I switch to using malloc() directly, compilation speed is 80% slower.

    And, on the same test, memory usage seems to be 20% higher (0.6GB vs
    0.5GB, but I can't measure accurately, because, well, there is no way to find out exactly how much malloc is using.)


    There is almost no conceivable situation where a program will be in any measurable way better by using 0.5 GB instead of 0.6 GB.


    Remember the subject is about /creating/ the allocation library. That
    means being able to specify how it will work. I would rather some of
    that work was offloaded to the allocator, since the author of the
    library may have a better idea of allocation strategies than I do.


    Did you miss the bit where I explained, several times, about how the
    implementation of "realloc" could use the extra space if it happens to
    be enough to satisfy the new request?  In such cases, realloc will be
    extremely fast.

    I'm sorry but I don't trust it to be fast. See how much longer malloc()
    took on my test above; nearly 50% of all runtime was spent inside
    malloc(); why is it so much slower?


    You are missing the point - deliberately, perhaps? A good "realloc"
    function will be extremely fast for cases where the "real" allocated
    size is enough for the new request. If it is not enough, then it has to
    do a new malloc() and then copy over the data - this will take time.
    (If you are doing this on Windows and using MS's C runtime DLL's, then /everything/ is slow because calling DLL functions has significant
    overhead.)


    It is also very common to have "local" allocators for small memory
    needs. Such allocators will be faster than OS-based ones, because they
    don't have to get memory from the OS, track ownership, handle multiple
    threads safely, etc.



    That sounds backwards. In what way is it to making assumptions to
    actually ASK what an implementation has reserved?

    Suppose I make an allocator that allocates everything in 128-byte
    blocks, tracked with bitmaps.  You ask for 800 bytes - I give you that
    by allocating 7 blocks, for a total of 896 bytes.  You now want to
    increase the size of the allocation, ideally to 1000 bytes.  You call
    the "get_real_size" function and find there are 96 bytes extra.  What
    is your code going to do now?  Maybe it thinks getting more than 896
    bytes is going to be slow, and makes do with just 896 bytes.  But that
    could be wrong - perhaps my allocator happens to have the next block
    free, so increasing to 1000 bytes would be almost instant - just a
    check and an OR on a bitmap.  Maybe it thinks it really needs the 1000
    bytes, in which case the call to "get_real_size" is completely
    wasted.  The information from "get_real_size" is /useless/.  It gives
    you nothing.

    But what is the cost to /me/, the implementer of the allocation
    system? If I have made a "get_real_size" function, and let you use it
    to get extra space, I have to stand by that - if I've told you that
    you really got 896 bytes, then I can't re-use the extra 96 bytes in
    any way. However, if I /don't/ give you such a function, then those 96
    bytes are still mine.  I can make version 2 of the library, with a
    sub-block allocator so that when you ask for 10 bytes, you get it from
    there.  The very act of providing a "get_real_size" function
    guarantees that those 96 bytes are wasted and cannot be used.

    It seems simple to me. If the allocator plans on using any reserved
    space, then get_real-size returns the exact requested, no extra.

    If it includes any extra on version 1, then on version 2 the behaviour
    of get_real_size can change to exclude it.


    Making it useless.

    The fact is that MY allocator DOES have the equivalent of
    `get_real_size`. (However, because it does not record block sizes, the
    info is put into a global - it could have been a function - that the
    caller must make use of before the next allocation. Or I can change the
    API so that the allocator returns a second pieces of information.)

    This works extremely well.

    So why is it that it can't work on any of the allocators you use? Or any that someone else might create? Maybe I should rewrite mine because according to use, it cannot work and cannot give any benefits!

    I think perhaps you're getting confused about what goes into the
    allocation library, what goes into the language implementation or
    runtime, and what goes into the user's application.


    No, I am not confused. And I fully agree that you should handle
    different aspects of allocations at different levels of a language
    system - I have said so several times.

    But I believe I am running out of new ways to explain to you the
    pointlessness of a "get_real_size" function in user code. It is worse
    than useless.



    No, byte-by-byte or int-by-int or record-by-record is what the
    user-program requests. Chunk-by-chunk is what the implementation has
    to do to get it efficient.


    No, that is not what user programs request for explicit low-level
    memory allocation.  Programs allocate in lumps if they can, at least
    for memory buffers that might be realloc'ed (which is what we are
    talking about here).


    It depends on the language. One that performs the allocations for you
    (which is most of them now), especially allocations that can grow, will
    have user-programs write to an array element at a time, and the implemention, if it allows that, will need to do so efficiently.

    And that means not leaving it 100% to the allocation library.






    --- Synchronet 3.19c-Linux NewsLink 1.113
  • From Bart@bc@freeuk.com to comp.lang.misc on Mon Nov 28 19:41:18 2022
    From Newsgroup: comp.lang.misc

    On 28/11/2022 19:01, David Brown wrote:
    On 28/11/2022 18:54, Bart wrote:
    On 28/11/2022 16:50, David Brown wrote:
    On 28/11/2022 15:04, Bart wrote:

    You don't want to know how much memory is being wasted on your behalf? >>>>

    When I need to know, I know - and not through a pointless
    "get_real_size" function, but by making sure I have an appropriate
    memory allocation system for the job.  When I have a big system -
    say, an embedded Linux board with 2 GB of memory - why would I care
    if an allocation is wasting 8 bytes or 16 bytes?  How could it
    possibly make a difference in real life?  You need to be doing
    /millions/ of allocations to have a waste that is possibly worth
    measuring.  (And if you are doing millions of allocations, you won't
    be doing it with malloc - you'll be using a specialised allocator
    optimised for the use-case.)

    My malloc() seems to use a minimum of 32 bytes per allocation. And yes
    people /do/ use malloc() without considering possible inefficiencies.

    Of course they do.  People use all sorts of things in their programming without considering possible inefficiencies - this is known as "using
    the right level of abstraction".  You always have a balance between
    maximum control to get the most efficient results (assuming - usually incorrectly - that the programmer knows enough to get optimal results),
    or relinquishing control for easier and faster development.  Do you
    think people should always worry about efficiency before every call to "printf"?  Or concern themselves about how their loop structures will interact with the cpu's branch prediction?  Of course not.  /Some/
    people, for certain code, need to worry about that kind of thing.  But
    for most programmers and most situations, the correct choice is to use
    the simplest source code and not worry.

    The same applies to memory allocation.  If you are worrying about a possible inefficiency of 32 bytes in a memory allocation, you are doing things /wrong/.

    If you are not worried about using 32 bytes instead of 16 or 8, then
    /you/ are getting it wrong. Why even bother using a lower level
    language; just use Python.


    (Note that implementing a language interpreter or run-time library

    And a compiler. And an image processing library. And ... pretty much everything which is computationally intensive.

    (One of my compilers uses my own allocator library. If I switch to
    using malloc() directly, compilation speed is 80% slower.

    And, on the same test, memory usage seems to be 20% higher (0.6GB vs
    0.5GB, but I can't measure accurately, because, well, there is no way
    to find out exactly how much malloc is using.)


    There is almost no conceivable situation where a program will be in any measurable way better by using 0.5 GB instead of 0.6 GB.

    Pointlessly using 20% more memory is fine? Except perhaps where the
    memory is already full.

    My own non-optimising compilers are rubbish because they generate code
    that runs at half the speed of gcc's. But using a memory allocator that
    slows down programs by that same amount is no problem!


    you are doing this on Windows and using MS's C runtime DLL's, then /everything/ is slow because calling DLL functions has significant overhead.)

    No, it doesn't. On x64 under Windows, a DLL call has to execute one
    extra instruction compared with a direct call, and even that could be
    fixed up properly, except the overhead is too insignificant to measure.

    (Really insignificant this time, not 80% more runtime and 20% more memory!)


    It is also very common to have "local" allocators for small memory
    needs.  Such allocators will be faster than OS-based ones, because they don't have to get memory from the OS, track ownership, handle multiple threads safely, etc.

    I'm sure malloc doesn't need to ask Windows for every tiny allocation.


    But I believe I am running out of new ways to explain to you the pointlessness of a "get_real_size" function in user code.  It is worse
    than useless.

    And I'm running out of ways to convince you that in my library, it works.

    Remember, yet again, that this is about building a new library where
    spare allocation capacitities can be a new feature.


    --- Synchronet 3.19c-Linux NewsLink 1.113
  • From Dmitry A. Kazakov@mailbox@dmitry-kazakov.de to comp.lang.misc on Mon Nov 28 20:59:51 2022
    From Newsgroup: comp.lang.misc

    On 2022-11-28 18:57, Bart wrote:
    On 28/11/2022 17:25, Dmitry A. Kazakov wrote:
    On 2022-11-28 17:48, Bart wrote:


    This would make that next allocation misaligned.

    If 1 was the alignment requested. The minimal interface of Allocate is

    - Size in storage elements
    - Alignment

    Note that pools are implemented in a huge variety of ways.

    Returning pointers which are not alligned to at least 64 bits would be
    very poor, if there is no way to control that.

    Rubbish, if I allocate an array of bits, there is no reason to align it.

    Calls to MS' VirtualAlloc are rounded up to a multiple of 4KB for
    fresh allocations.

    VirtualAlloc is not a memory pool allocator. It is a part of paged
    memory API.

    I don't care how it works.

    Again, it is NOT a memory pool allocator. Do not use the toilet cleaner
    to brush your teeth!
    --
    Regards,
    Dmitry A. Kazakov
    http://www.dmitry-kazakov.de

    --- Synchronet 3.19c-Linux NewsLink 1.113
  • From Bart@bc@freeuk.com to comp.lang.misc on Mon Nov 28 21:02:35 2022
    From Newsgroup: comp.lang.misc

    On 28/11/2022 19:59, Dmitry A. Kazakov wrote:
    On 2022-11-28 18:57, Bart wrote:
    On 28/11/2022 17:25, Dmitry A. Kazakov wrote:
    On 2022-11-28 17:48, Bart wrote:


    This would make that next allocation misaligned.

    If 1 was the alignment requested. The minimal interface of Allocate is

    - Size in storage elements
    - Alignment

    Note that pools are implemented in a huge variety of ways.

    Returning pointers which are not alligned to at least 64 bits would be
    very poor, if there is no way to control that.

    Rubbish, if I allocate an array of bits, there is no reason to align it.

    Suppose it isn't an array of bits? The allocators we're talking about
    don't know anything about what types are to be stored in them.

    And if it was an array of bits, they would still benefit greatly from
    being 8-byte aligned, since operations such as clear, copy, compare etc
    can be done more efficiently.

    (I implement bit arrays and bit-sets, as sets of so many 64-bit words.
    It is only views of bit-slices that may start off in the middle of a word.

    I also implement arrays of 2 and 4 bits, and those additionally need
    elements aligned within a byte, even for slices.)

    Calls to MS' VirtualAlloc are rounded up to a multiple of 4KB for
    fresh allocations.

    VirtualAlloc is not a memory pool allocator. It is a part of paged
    memory API.

    I don't care how it works.

    Again, it is NOT a memory pool allocator. Do not use the toilet cleaner
    to brush your teeth!

    I don't even know what that is. My applications just want a memory block allocated for them.

    Can I write to data within the block? Can I read from it? (Can I execute
    in some cases?) Can I free it?

    Ergo it is the same as any other allocator as far as an application is concerned.



    --- Synchronet 3.19c-Linux NewsLink 1.113
  • From BGB@cr88192@gmail.com to comp.lang.misc on Mon Nov 28 15:09:35 2022
    From Newsgroup: comp.lang.misc

    On 11/27/2022 11:06 AM, Andy Walker wrote:
    On 25/11/2022 16:15, David Brown wrote:
    I think you should not try to copy C's malloc/free mechanism.

        Perhaps worth noting that there are two different sorts of "free" mechanism:

     (a) When the /programmer/ decides that some storage is no longed needed
         and therefore decides it can be re-used.  Obviously, there are some
         applications where this is reliable.  But in general it is unsafe.
         The problems arise when there are graph structures where nodes of
         the graph may be pointed at directly or indirectly by virtue of
         some pointer accessing a component of the node.  This is difficult
         programming, and programmers often get it wrong, leading to perhaps
         subtle bugs, or else don't bother, leading to poor performance as
         storage is retained that could be freed.

     (b) When the storage manager for the compiler/library decides that some
         storage is no-longer needed.  IOW, when garbage collection, or near
         equivalent, re-organises storage and perhaps returns some to the
         OS.  This is also difficult, but is a solved problem so should be
         tried and tested for your language and thus reliable.

    Summary:  You will deduce that my opinion is that systems/languages
    that rely on people to write programs that get "free" right are a
    disaster waiting to happen  You could perhaps regard "free" as a hint
    that you expect storage to be garbage-collectible, but the final
    decision should always be taken by a proper garbage collector.



    Great problem with GC is that there isn't really any good way to do it
    in a way that does not have adverse effects on either performance and/or latency.

    These issues tend to make it a deal-breaker for things like hard real
    time applications (such as motor-controls or reading data from optical
    or magnetic sensors, ...). In these applications, typically latency requirements are measured in terms of microseconds.

    These sorts of requirements mostly exclude GC, and also most languages
    with requirements much higher than those needed to run C (one is often
    dealing with CPUs with clock-speeds measured in MHz and RAM sizes
    measured in kB).


    For user facing applications (on a PC class CPU), GC may be more
    acceptable, though often the latency still needs to be kept within the
    low millisecond territory. Say, for example, one is trying to update the screen at 60fps or similar, and any unexpected delay over 5ms may risk disrupting the frame-rate.

    The challenge then is having a GC that is both effective and can
    reliably keep delays under 1-5 ms.


    But, say, if one wants a language that can, say:
    Be used to implement an OS kernel;
    Be used for real-time motor controls or similar;
    ...

    There isn't a whole lot of choice here.

    One is mostly limited to things like malloc/free and a zone allocator
    (major common example of the latter being the original Doom engine, 1).

    *1: One can allocate memory with a "zone tag" giving a category as to
    what part of the engine it applies to. When doing a task such as a map
    change, any objects allocated in a zone below that of the current map
    can be freed in bulk.


    One other option being "automatic" allocate and free, where the memory allocation can be tied to the stack frame where it was allocated, so
    when the parent function returns the associated memory is freed implicitly.

    In my C compiler, I had used a similar sort of mechanism for "alloca"
    and also VLAs, where they are heap allocated, but automatically freed
    when the caller returns. In this case, the compiler+runtime maintains a
    linked list (for any functions which call "alloca()" or similar), and
    then frees any objects held in this list when this function returns.


    My BGBScript2 language uses a similar scheme, just with "new":
    new Type; //normal new, heap allocated.
    new! Type; //automatic, destroyed when parent function returns
    new(ZTag) Type; //heap allocated, assigned to a zone.

    The memory objects also maintain a type-tag, so (given a pointer) it is possible to determine what type of object it is (this type-tag is
    generally also present in the high bits of the pointer).

    Though, for arrays, the situation gets a little more complicated.



    Ironically, despite in my current project it having initially seemed as
    if C had "won the war", in some of my more recent stuff BS2 is starting
    to seemingly "claw its way back into relevance" (mostly in corners that
    C does not address all that effectively). BS2 had been mostly "on the
    back burner" for most of my current project (whereas BS and BS2 had
    played a bigger role in my 3D engine projects, for mostly "bare metal" programming on a custom CPU ISA, it faces more direct competition mostly
    from C and ASM).

    Though, the situation in this case is a little different than for
    traditional HLLs, in that both C and BS2 share the same compiler and ABI
    (so actually most of the difference is more in terms of syntax and
    language rules).

    In past incarnations (on x86-64 and similar), both BS and BS2 had
    typically run in VMs (which, granted, do not share the C ABI).

    In the BS2 case, the 'native' keyword was used for a mechanism along
    similar lines to P/Invoke in C# (which was at least slightly less
    terrible than something like the JVM's JNI or similar). There was also a partial wrapper-glue tool to be able to export functions from BS2 to C.

    No glue generation is needed for BGBCC, where in this case 'native' is
    mostly understood as "use the bare C ABI here" (so in this case
    functions more like 'extern "C"' in C++).


    Though, at present, BGBCC is only targeting my own ISA (my BJX2 ISA).

    Had considered an attempt to back-port my compiler to be able to target x86-64, but this has gone nowhere thus far (not a huge incentive to do
    so at the moment).

    I did at one point attempt limited code generation on ARMv7 / Thumb2
    (though, in this case, by using a partial fork of my BJX2 emulator as a
    sort of AOT), but results were "not promising" (performance was
    "extremely bad" if compared with native GCC output).

    Other more specialized code-generation attempts were not promising, as
    it seems almost like some sort of "magic ingredient" is needed to get acceptable performance out of ARMv7.

    Though, ARMv7's relatively cramped register space does result in codegen output that consists almost entirely of a wall of load and store
    operations from all the register spill and fill (and GCC seems to do a somewhat better job here).

    ...

    --- Synchronet 3.19c-Linux NewsLink 1.113
  • From Dmitry A. Kazakov@mailbox@dmitry-kazakov.de to comp.lang.misc on Mon Nov 28 22:17:07 2022
    From Newsgroup: comp.lang.misc

    On 2022-11-28 22:02, Bart wrote:
    On 28/11/2022 19:59, Dmitry A. Kazakov wrote:
    On 2022-11-28 18:57, Bart wrote:
    On 28/11/2022 17:25, Dmitry A. Kazakov wrote:
    On 2022-11-28 17:48, Bart wrote:


    This would make that next allocation misaligned.

    If 1 was the alignment requested. The minimal interface of Allocate is >>>>
    - Size in storage elements
    - Alignment

    Note that pools are implemented in a huge variety of ways.

    Returning pointers which are not alligned to at least 64 bits would
    be very poor, if there is no way to control that.

    Rubbish, if I allocate an array of bits, there is no reason to align it.

    Suppose it isn't an array of bits? The allocators we're talking about
    don't know anything about what types are to be stored in them.

    Not we. I am talking about a reasonable pool interface.

    And if it was an array of bits, they would still benefit greatly from
    being 8-byte aligned, since operations such as clear, copy, compare etc
    can be done more efficiently.

    No, because if packed bit-array supports slicing, all operations must
    expect bit-aligned data anyway.
    --
    Regards,
    Dmitry A. Kazakov
    http://www.dmitry-kazakov.de

    --- Synchronet 3.19c-Linux NewsLink 1.113
  • From Bart@bc@freeuk.com to comp.lang.misc on Mon Nov 28 21:34:00 2022
    From Newsgroup: comp.lang.misc

    On 28/11/2022 21:17, Dmitry A. Kazakov wrote:
    On 2022-11-28 22:02, Bart wrote:

    And if it was an array of bits, they would still benefit greatly from
    being 8-byte aligned, since operations such as clear, copy, compare
    etc can be done more efficiently.

    No, because if packed bit-array supports slicing, all operations must
    expect bit-aligned data anyway.

    The original bit-array that is heap-allocated will be u64-aligned,
    always. An allocator is never going to start such a thing 2/3 through a
    word; that would be totally ludicrous.

    When you take a slice, it will be of a bit-array that already exists, so
    that allocation doesn't come it it.

    But the slice takes the form of a BYTE-pointer and a bit offset;
    pointers to individual bits don't exist on current hardware.

    So first bit of the bit-slice may not start at bit 0 of the
    byte-pointer; it is not aligned at the bit-level.

    (My bit-arrays have elements of 1/2/4 bits, which are aligned within qa
    byte such that an 2/4-bit element will not straddle a byte.)
    --- Synchronet 3.19c-Linux NewsLink 1.113
  • From Dmitry A. Kazakov@mailbox@dmitry-kazakov.de to comp.lang.misc on Tue Nov 29 08:45:32 2022
    From Newsgroup: comp.lang.misc

    On 2022-11-28 22:34, Bart wrote:
    On 28/11/2022 21:17, Dmitry A. Kazakov wrote:
    On 2022-11-28 22:02, Bart wrote:

    And if it was an array of bits, they would still benefit greatly from
    being 8-byte aligned, since operations such as clear, copy, compare
    etc can be done more efficiently.

    No, because if packed bit-array supports slicing, all operations must
    expect bit-aligned data anyway.

    The original bit-array that is heap-allocated will be u64-aligned,
    always.

    I have several specialized heap implementations that do not impose
    additional alignment.

    It is a pointless discussion, really.
    --
    Regards,
    Dmitry A. Kazakov
    http://www.dmitry-kazakov.de

    --- Synchronet 3.19c-Linux NewsLink 1.113
  • From David Brown@david.brown@hesbynett.no to comp.lang.misc on Tue Nov 29 09:10:50 2022
    From Newsgroup: comp.lang.misc

    On 28/11/2022 20:41, Bart wrote:
    On 28/11/2022 19:01, David Brown wrote:
    On 28/11/2022 18:54, Bart wrote:
    On 28/11/2022 16:50, David Brown wrote:
    On 28/11/2022 15:04, Bart wrote:

    You don't want to know how much memory is being wasted on your behalf? >>>>>

    When I need to know, I know - and not through a pointless
    "get_real_size" function, but by making sure I have an appropriate
    memory allocation system for the job.  When I have a big system -
    say, an embedded Linux board with 2 GB of memory - why would I care
    if an allocation is wasting 8 bytes or 16 bytes?  How could it
    possibly make a difference in real life?  You need to be doing
    /millions/ of allocations to have a waste that is possibly worth
    measuring.  (And if you are doing millions of allocations, you won't >>>> be doing it with malloc - you'll be using a specialised allocator
    optimised for the use-case.)

    My malloc() seems to use a minimum of 32 bytes per allocation. And
    yes people /do/ use malloc() without considering possible
    inefficiencies.

    Of course they do.  People use all sorts of things in their
    programming without considering possible inefficiencies - this is
    known as "using the right level of abstraction".  You always have a
    balance between maximum control to get the most efficient results
    (assuming - usually incorrectly - that the programmer knows enough to
    get optimal results), or relinquishing control for easier and faster
    development.  Do you think people should always worry about efficiency
    before every call to "printf"?  Or concern themselves about how their
    loop structures will interact with the cpu's branch prediction?  Of
    course not.  /Some/ people, for certain code, need to worry about that
    kind of thing.  But for most programmers and most situations, the
    correct choice is to use the simplest source code and not worry.

    The same applies to memory allocation.  If you are worrying about a
    possible inefficiency of 32 bytes in a memory allocation, you are
    doing things /wrong/.

    If you are not worried about using 32 bytes instead of 16 or 8, then
    /you/ are getting it wrong. Why even bother using a lower level
    language; just use Python.


    Let me say it again. If you are worried about possible 32 byte
    inefficiencies in a memory allocation, you are doing things /wrong/.

    Understand the logic of that sentence.

    Perhaps you have plenty of resources - PC programming. A mere 32 bytes
    waste is irrelevant, and you should be spending your brain cells on
    things that are actually important. (And yes, you might well be better
    off in a higher level language.)

    If you /don't/ have plenty of resources, such as small-systems embedded programming, or you are make so many allocations that a waste of up to
    32 bytes per allocation is relevant, then you should not be using that general-purpose allocator. You should be using a system where you
    /know/ how much, or how little, is wasted, and where you know the waste
    is small enough that it is not a problem.

    Ergo, if you are worried about possible 32 byte inefficiencies in a
    memory allocation, you are doing things /wrong/.



    There is almost no conceivable situation where a program will be in
    any measurable way better by using 0.5 GB instead of 0.6 GB.

    Pointlessly using 20% more memory is fine? Except perhaps where the
    memory is already full.


    You are bike-shedding. (I hope you know the term - it applies to a lot
    of what you write.) Concern yourself with things that are important,
    and make a real difference.

    PC ram costs - what, $10 per GB? That wasted 0.1 GB is a dollar's worth
    of waste. If a customer has to pay the programmer to improve the
    program to save 20% memory in allocations - how much will /that/ cost?
    How much market share will be lost while waiting for the programmer to
    finish, turning the "good enough for the purpose" program into a
    something that is technically better but /exactly/ the same from the
    users' viewpoint? What is the cost of the subtle bug in the
    programmer's smarter memory allocator that causes users' data to be
    corrupted a couple of times a month?

    You worry about utterly irrelevant details of efficiency or speed, yet
    say you consider bugs and incorrect code just a fact of life in
    programming and don't worry about that. You have the whole thing ass-backwards. Worry far more about code /correctness/ - avoiding bugs,
    or at least making it easier to find bugs - and far less about minor inefficiencies that do not affect code usability.

    --- Synchronet 3.19c-Linux NewsLink 1.113
  • From James Harris@james.harris.1@gmail.com to comp.lang.misc on Tue Nov 29 09:50:31 2022
    From Newsgroup: comp.lang.misc

    On 26/11/2022 13:18, Bart wrote:
    On 26/11/2022 11:31, James Harris wrote:

    ...

    It's an interesting topic. I can think of three really fast memory
    allocation approaches:

    1. sbrk-like
    2. stack
    3. array of slots, with occupied/vacant essentially indicated by a
    bitmap.

    The sbrk-like approach (taking memory as needed and not returning it)
    can work well if all allocations can remain until the program terminates.

    The stack approach can work well if allocations can be deallocated in
    reverse order.

    The array of slots can work well if the things to allocate are of the
    same size.

    ...

    Your suggestion of requiring the caller to remember how large the
    allocation is is intriguing and it sounds rather like what mmap
    expects. But for strings how would it work, especially if a string's
    length is not known in advance?

    How would strings work now with malloc? That still requires the length!

    You know how strings work with malloc; you gave a good example thereof!
    What I was saying is that none of the three fast schemes mentioned above
    are generally suitable for strings.


    If you have counted strings, then you will know their length to free
    them. Otherwise use strlen. But /something/ has to record the length.

    For efficiency, one may want to distinguish between length and capacity. Memory management would manage capacity changes. The programmer (or a
    string library) would concern himself/itself with the length and the
    capacity.


    You could use special routines just for strings, that say store a length
    in the space just before the returned pointer. Then you can also have a routine to return that length, saving you some hassle. But all you've
    done now really is create a mini string library.

    I'd have the /length/ in a control block but memory allocation would
    only be interested in the /capacity/.


    Further, what if the caller requested 100 bytes then told the
    allocator to free 99 bytes? What if it told the allocator to free 101
    bytes?

    Then it'll go wrong. You could also pass the wrong pointer to the deallocator. Or allocate the space in the first place with 99 instead of 100; what stops you doing that?

    A deallocator can check that it's been passed a valid pointer.

    ...

    It's also quite easy to create custom allocators for special purposes.
    For example if you are dealing with millions of identical small objects which are being constantly created and destroyed, allocate a large pool
    of memory to hold them.

    Agreed.


    Then all freed objects can be linked together; allocating a new object
    is very fast, and deallocating instant: add this object to the head of
    the free-list.

    You can also deallocate the lot in one go.

    I've never been comfortable with freeing loads of allocations in one go
    except when the program terminates but I take your point.
    --
    James Harris


    --- Synchronet 3.19c-Linux NewsLink 1.113
  • From James Harris@james.harris.1@gmail.com to comp.lang.misc on Tue Nov 29 10:15:22 2022
    From Newsgroup: comp.lang.misc

    On 26/11/2022 13:46, Dmitry A. Kazakov wrote:
    On 2022-11-26 14:15, James Harris wrote:
    On Friday, 25 November 2022 at 17:21:11 UTC, Dmitry A. Kazakov wrote:

    ...

    A related important issue is interaction between pointers from different >>> pools and universal pointers applicable to all pools including locally
    allocated objects.

    ...

    You seem to be talking about pools being 'NUMA pools' where each pool
    is of bytes but has certain access characteristics such as bandwidth,
    latency, and contention. Is that right?

    Just a pool = heap. E.g. under Windows an application may have a number
    of different heaps.

    In that case it might be better to call them heaps. Pool is already overloaded.
    --
    James Harris


    --- Synchronet 3.19c-Linux NewsLink 1.113
  • From James Harris@james.harris.1@gmail.com to comp.lang.misc on Tue Nov 29 10:27:42 2022
    From Newsgroup: comp.lang.misc

    On 27/11/2022 15:18, David Brown wrote:
    On 25/11/2022 20:23, Bart wrote:

    ...

    A "resize" function would always have code equivalent to first calling "get_real_size" and then seeing if an actual re-allocation and copy is needed.  It would be pointless for user code to duplicate this functionality.  Therefore, it is pointless to have a "get_real_size" function available to the user code - using it would be less efficient
    than simply calling "resize" directly.

    Two points of disagreement on that.

    1. A program which knows the capacity of an allocation can iterate up to
    the capacity without incurring the overhead of a function call.

    2. A program which knows the capacity can also decide whether to append
    to the existing section of the string, move the existing section, or to
    start a new section.


    But there is another reason; take a block which will grow a byte at a
    time. Apart from needing to call 'resize' for every byte, who knows
    how much spare capacity 'resize' will actually provide; it may need to
    do real reallocations more often.

    ...

    Trying to optimise your interface around pathological examples is
    madness.  Design the interface to be useful for real, practical code.

    Sometimes the only way to design a new system is to work out the ways in
    which it might be used.

    Further, appending to a string is a very common use case.
    --
    James Harris


    --- Synchronet 3.19c-Linux NewsLink 1.113
  • From James Harris@james.harris.1@gmail.com to comp.lang.misc on Tue Nov 29 10:43:04 2022
    From Newsgroup: comp.lang.misc

    On 28/11/2022 14:04, Bart wrote:
    On 28/11/2022 10:17, David Brown wrote:

    ...

    I cannot see any point in first asking how much space I /really/ have,
    before asking for the 1020 bytes.  What is the point?

    You're never curious about how much spare capacity was actually set
    aside for your request? You don't think there is any way to profitably exploit that?

    It's not about curiosity but about transparency. And greater
    transparency enables greater efficiency.

    ...

      That would be lying
    to the allocator.  When people start doing that kind of thing, it
    means they are making undocumented assumptions about implementations -
    and that means implementations get stuck with with these limitations,
    or user code breaks.

    I wanted to pick up on David talking about 'lying' to the allocator.
    That's a misleadingly emotive term probably based on the idea that the allocation call requests exactly N bytes. Instead, the initial request,
    in this case, would be for "at least N". Asking "how many" is not in any
    way lying.
    --
    James Harris


    --- Synchronet 3.19c-Linux NewsLink 1.113
  • From James Harris@james.harris.1@gmail.com to comp.lang.misc on Tue Nov 29 10:48:45 2022
    From Newsgroup: comp.lang.misc

    On 28/11/2022 19:59, Dmitry A. Kazakov wrote:

    ...

    Again, it is NOT a memory pool allocator. Do not use the toilet cleaner
    to brush your teeth!

    You seem to be thinking a lot about toilets lately, Dmitry. Recent
    problems with the plumbing?

    ;-)
    --
    James Harris


    --- Synchronet 3.19c-Linux NewsLink 1.113
  • From James Harris@james.harris.1@gmail.com to comp.lang.misc on Tue Nov 29 10:50:09 2022
    From Newsgroup: comp.lang.misc

    On 28/11/2022 16:50, David Brown wrote:
    On 28/11/2022 16:03, Dmitry A. Kazakov wrote:
    On 2022-11-28 15:04, Bart wrote:

    You don't want to know how much memory is being wasted on your behalf?

    https://man7.org/linux/man-pages/man3/mallinfo.3.html

    It looks like you want to assume the allocator will set aside zero
    spare capacity,

    That is a reasonable assumption. Note that frequently dynamically
    allocated library data types are designed to work with all possible
    implementations of pools, e.g. a pool is taken as a parameter of a
    generic or OO instance. Therefore the weakest assumption is made.


    No, the weakest assumption (which is the one made by most programmers)
    is that you do not know anything about how much wasted or spare capacity there might be - you assume only that the allocation function does what
    it says it will do.  So when you ask for 100 bytes, you assume nothing
    more than that you have 100 bytes.  You don't assume you might have
    spare memory, or wasted memory - but you also do not assume that there
    is zero extra or wasted memory.  You assume nothing at all, and rely
    solely on the specification of the allocation functions.

    Don't ask for 100 bytes. Ask for at least 100.
    --
    James Harris


    --- Synchronet 3.19c-Linux NewsLink 1.113
  • From Dmitry A. Kazakov@mailbox@dmitry-kazakov.de to comp.lang.misc on Tue Nov 29 12:17:20 2022
    From Newsgroup: comp.lang.misc

    On 2022-11-29 11:48, James Harris wrote:
    On 28/11/2022 19:59, Dmitry A. Kazakov wrote:

    ...

    Again, it is NOT a memory pool allocator. Do not use the toilet
    cleaner to brush your teeth!

    You seem to be thinking a lot about toilets lately, Dmitry. Recent
    problems with the plumbing?

    Rather a reference to "Dennis the Menace" 1993.
    --
    Regards,
    Dmitry A. Kazakov
    http://www.dmitry-kazakov.de

    --- Synchronet 3.19c-Linux NewsLink 1.113
  • From Bart@bc@freeuk.com to comp.lang.misc on Tue Nov 29 11:49:58 2022
    From Newsgroup: comp.lang.misc

    On 29/11/2022 10:27, James Harris wrote:
    On 27/11/2022 15:18, David Brown wrote:
    On 25/11/2022 20:23, Bart wrote:

    ...

    A "resize" function would always have code equivalent to first calling
    "get_real_size" and then seeing if an actual re-allocation and copy is
    needed.  It would be pointless for user code to duplicate this
    functionality.  Therefore, it is pointless to have a "get_real_size"
    function available to the user code - using it would be less efficient
    than simply calling "resize" directly.

    Two points of disagreement on that.

    1. A program which knows the capacity of an allocation can iterate up to
    the capacity without incurring the overhead of a function call.

    2. A program which knows the capacity can also decide whether to append
    to the existing section of the string, move the existing section, or to start a new section.


    But there is another reason; take a block which will grow a byte at a
    time. Apart from needing to call 'resize' for every byte, who knows
    how much spare capacity 'resize' will actually provide; it may need
    to do real reallocations more often.

    ...

    Trying to optimise your interface around pathological examples is
    madness.  Design the interface to be useful for real, practical code.

    Sometimes the only way to design a new system is to work out the ways in which it might be used.

    Further, appending to a string is a very common use case.


    Trying to argue with David Brown has really worn me down on this.

    - Everything I say is wrong. Every example I give is a pathological one
    that is not worth looking at (it's 'madness'). Who grows a string, list
    etc an element at a time

    - Using twice or four times the memory for a small allocation is
    irrelevant: "If you are worrying about a possible inefficiency of 32
    bytes in a memory allocation, you are doing things /wrong/."

    - Having an allocator being slow enough to take up nearly half the
    runtime of the entire application, not just the memory allocation bits,
    is irrelevant.

    - "Using a 'get_real_size` function is pointless" (even in my own
    allocator which has spare capacity, designed to be interrogated, as a feature!)

    - A good 'realloc' function will be extremely fast

    - Windows/MS/DLLs are slow anyway, what do you expect? [Incorrect]

    - Avoid dynamic memory completely if it's that critical

    This is on top of his opinion that super-fast compilers that are 10-100
    times faster than typical (partly thanks to people sweating the small
    stuff) are pointless and irrelevant. Or if it's something I've done,
    that nobody cares.

    One wonders what the hell he's doing in a language design group where
    all these things are up for grabs



    --- Synchronet 3.19c-Linux NewsLink 1.113
  • From James Harris@james.harris.1@gmail.com to comp.lang.misc on Tue Nov 29 12:49:33 2022
    From Newsgroup: comp.lang.misc

    On 28/11/2022 16:50, David Brown wrote:
    On 28/11/2022 15:04, Bart wrote:
    On 28/11/2022 10:17, David Brown wrote:
    On 27/11/2022 19:18, Bart wrote:

    ...

    Remember the subject is about /creating/ the allocation library. That
    means being able to specify how it will work. I would rather some of
    that work was offloaded to the allocator, since the author of the
    library may have a better idea of allocation strategies than I do.


    Did you miss the bit where I explained, several times, about how the implementation of "realloc" could use the extra space if it happens to
    be enough to satisfy the new request?  In such cases, realloc will be extremely fast.

    Could you have holed your own argument below the waterline? You mention realloc being "extremely fast" when it has room to expand so you appear
    to acknowledge that it could be relatively slow when there is not enough
    room. The problem, I would suggest, is that under your scheme an
    application would not know whether a particular call to realloc was
    going to get the fast or the slow response. Correct?

    So the very call which needed a fast response /could not use/ your
    realloc because it would have no way to know whether the allocator's
    response would be fast or slow.

    In other words, there may be a fast realloc sitting there available to
    the program but it could not take advantage of it the very kind of call
    which needs the speed. By contrast, under my proposal a program /could/
    take advantage of the extra speed - and more. See below.

    ...

    That sounds backwards. In what way is it to making assumptions to
    actually ASK what an implementation has reserved?


    You are assuming particular characteristics of the allocator that are inappropriate for a general-purpose memory allocator.  All a low-level general allocator needs to do is have a function to allocate memory, a function to release memory, and optionally (but usefully) a function to increase an allocation efficiently.  Every function you include beyond that, every detail in specifications, is a limitation on the flexibility
    of the allocator.

    Here's an example.

    Suppose I make an allocator that allocates everything in 128-byte
    blocks, tracked with bitmaps.

    It seems odd to use fixed-size slots for variably sized objects. And 128
    bytes is rather coarse but I will, below, apply my proposed scheme to
    your scenario.

    BTW, I can tell you are a professional as you expect us to have the
    128-times table in our heads!


    You ask for 800 bytes - I give you that
    by allocating 7 blocks, for a total of 896 bytes.  You now want to
    increase the size of the allocation, ideally to 1000 bytes.  You call
    the "get_real_size" function and find there are 96 bytes extra.  What is your code going to do now?  Maybe it thinks getting more than 896 bytes
    is going to be slow, and makes do with just 896 bytes.  But that could
    be wrong - perhaps my allocator happens to have the next block free, so increasing to 1000 bytes would be almost instant - just a check and an
    OR on a bitmap.  Maybe it thinks it really needs the 1000 bytes, in
    which case the call to "get_real_size" is completely wasted.  The information from "get_real_size" is /useless/.  It gives you nothing.

    Under the proposal, if the code had to have the 1000 bytes as a single
    run it would call m_realloc(). That would extend the 896 to 1024 if
    there was space. If not, it would create a new allocation and move the original there. This is the same as C's realloc().

    By contrast, if the code did not require the 1000 bytes to be in a
    single run it would see that the current run was 896 and would call
    m_resize() to ask whether the run could be extended in place. As before,
    if there was space available the allocator would increase the allocation
    to 1024. If there was not enough space, however, (i.e. if the next block
    was occupied) it would leave the allocation as 896. The caller would
    then create a new allocation for the rest of the space it needed. (The
    caller would likely also link the two allocations together.)

    Note that under the proposal a program could still go with the simple
    realloc if that's what it wanted to do but it would also have other options.
    --
    James Harris


    --- Synchronet 3.19c-Linux NewsLink 1.113
  • From James Harris@james.harris.1@gmail.com to comp.lang.misc on Tue Nov 29 12:55:00 2022
    From Newsgroup: comp.lang.misc

    On 29/11/2022 08:10, David Brown wrote:
    On 28/11/2022 20:41, Bart wrote:
    On 28/11/2022 19:01, David Brown wrote:

    ...

    The same applies to memory allocation.  If you are worrying about a
    possible inefficiency of 32 bytes in a memory allocation, you are
    doing things /wrong/.

    If you are not worried about using 32 bytes instead of 16 or 8, then
    /you/ are getting it wrong. Why even bother using a lower level
    language; just use Python.


    Let me say it again.  If you are worried about possible 32 byte inefficiencies in a memory allocation, you are doing things /wrong/.

    I cannot speak for Bart but AISI the problem isn't space efficiency but
    time efficiency. If you want to extend a string but there's no room to
    expand it where it is you may prefer to request space for a new section
    of the string rather than to copy all the existing content.
    --
    James Harris


    --- Synchronet 3.19c-Linux NewsLink 1.113
  • From David Brown@david.brown@hesbynett.no to comp.lang.misc on Tue Nov 29 15:55:51 2022
    From Newsgroup: comp.lang.misc

    On 29/11/2022 11:50, James Harris wrote:
    On 28/11/2022 16:50, David Brown wrote:
    On 28/11/2022 16:03, Dmitry A. Kazakov wrote:
    On 2022-11-28 15:04, Bart wrote:

    You don't want to know how much memory is being wasted on your behalf?

    https://man7.org/linux/man-pages/man3/mallinfo.3.html

    It looks like you want to assume the allocator will set aside zero
    spare capacity,

    That is a reasonable assumption. Note that frequently dynamically
    allocated library data types are designed to work with all possible
    implementations of pools, e.g. a pool is taken as a parameter of a
    generic or OO instance. Therefore the weakest assumption is made.


    No, the weakest assumption (which is the one made by most programmers)
    is that you do not know anything about how much wasted or spare
    capacity there might be - you assume only that the allocation function
    does what it says it will do.  So when you ask for 100 bytes, you
    assume nothing more than that you have 100 bytes.  You don't assume
    you might have spare memory, or wasted memory - but you also do not
    assume that there is zero extra or wasted memory.  You assume nothing
    at all, and rely solely on the specification of the allocation functions.

    Don't ask for 100 bytes. Ask for at least 100.


    No, if you need 100 bytes, ask for 100 bytes. If you need more, ask for
    more.

    The implementation can satisfy the request by giving more than 100
    bytes, but that is irrelevant to the application - it's just an
    implementation detail.



    --- Synchronet 3.19c-Linux NewsLink 1.113
  • From David Brown@david.brown@hesbynett.no to comp.lang.misc on Tue Nov 29 16:00:54 2022
    From Newsgroup: comp.lang.misc

    On 29/11/2022 13:55, James Harris wrote:
    On 29/11/2022 08:10, David Brown wrote:
    On 28/11/2022 20:41, Bart wrote:
    On 28/11/2022 19:01, David Brown wrote:

    ...

    The same applies to memory allocation.  If you are worrying about a
    possible inefficiency of 32 bytes in a memory allocation, you are
    doing things /wrong/.

    If you are not worried about using 32 bytes instead of 16 or 8, then
    /you/ are getting it wrong. Why even bother using a lower level
    language; just use Python.


    Let me say it again.  If you are worried about possible 32 byte
    inefficiencies in a memory allocation, you are doing things /wrong/.

    I cannot speak for Bart but AISI the problem isn't space efficiency but
    time efficiency. If you want to extend a string but there's no room to expand it where it is you may prefer to request space for a new section
    of the string rather than to copy all the existing content.


    Pick one or the other - either you want to keep your strings as single contiguous buffers, or you use a chain of buffers. In the first
    instance, you use realloc and let it copy the data if needed. In the
    second case, you use sections of a fixed size and malloc them - you
    don't expand them.

    In either case, messing around with trying to expand only when
    "get_real_size" indicates it is cheap will lead to more complicated code
    that is regularly slower, and is only occasionally fast (and the realloc
    will be fast too in those cases).



    --- Synchronet 3.19c-Linux NewsLink 1.113
  • From Bart@bc@freeuk.com to comp.lang.misc on Tue Nov 29 16:30:34 2022
    From Newsgroup: comp.lang.misc

    On 29/11/2022 10:43, James Harris wrote:
    On 28/11/2022 14:04, Bart wrote:
    On 28/11/2022 10:17, David Brown wrote:

    ...

    I cannot see any point in first asking how much space I /really/
    have, before asking for the 1020 bytes.  What is the point?

    You're never curious about how much spare capacity was actually set
    aside for your request? You don't think there is any way to profitably
    exploit that?

    It's not about curiosity but about transparency. And greater
    transparency enables greater efficiency.

    It depends on what you intend doing with a function like 'get_real_size'.

    If you want to rely on it for helping implement expanding buffers
    efficiently, then you need to know that the allocator will give you a worthwhile amount of spare capacity.

    With a single, general-purpose allocator, there is no reason for it to
    do that; it will only round up enough to meet alignment needs.

    So it might be better to two have kinds of allocator: fixed size, and expanding.

    The latter is a bit like your 'at least N' suggestion, but that's not
    strong enough I think. It needs to be specifically suitable for growing.

    My own allocator was a special-purpose one for an interpreter:
    allocations up to 33MB were rounded up to the next power of two size,
    with a minimum buffer size of 16 bytes.

    I use it from ordinary programs as well, but there it could do with an fixed-size allocator too (which would only round to the next multiple of
    8). There however I probably wouldn't support Free, or not effectively.



    --- Synchronet 3.19c-Linux NewsLink 1.113
  • From David Brown@david.brown@hesbynett.no to comp.lang.misc on Tue Nov 29 17:54:15 2022
    From Newsgroup: comp.lang.misc

    On 29/11/2022 11:43, James Harris wrote:
    On 28/11/2022 14:04, Bart wrote:
    On 28/11/2022 10:17, David Brown wrote:

    ...

    I cannot see any point in first asking how much space I /really/
    have, before asking for the 1020 bytes.  What is the point?

    You're never curious about how much spare capacity was actually set
    aside for your request? You don't think there is any way to profitably
    exploit that?

    It's not about curiosity but about transparency. And greater
    transparency enables greater efficiency.

    ...

       That would be lying
    to the allocator.  When people start doing that kind of thing, it
    means they are making undocumented assumptions about implementations
    - and that means implementations get stuck with with these
    limitations, or user code breaks.

    I wanted to pick up on David talking about 'lying' to the allocator.
    That's a misleadingly emotive term probably based on the idea that the allocation call requests exactly N bytes. Instead, the initial request,
    in this case, would be for "at least N". Asking "how many" is not in any
    way lying.


    Allocation functions like "malloc" do not ask for "at least N bytes".
    They ask for "N bytes". And the allocator gives you "N bytes" (if it
    can). What you get is a pointer and the guarantee that you can use N
    bytes from that pointer. You don't get to use any more than N bytes,
    even if the allocation results in M extra bytes being left unusable at
    the end of the N bytes. Using those M extra bytes without telling the allocator (by "realloc") is, if not "lying", at least making unwarranted assumptions and misusing the space behind the back of the allocator that
    owns the space.

    Programming is all about specifications. "malloc" is specified to
    return a pointer to N bytes. That is /all/. If you start messing
    around by assuming more than that, or trying to sneak out extra
    information, your code is now in the category of "works by luck" instead
    of "works by design". Lots of people program by luck, but it is hardly something to aspire to or encourage.


    --- Synchronet 3.19c-Linux NewsLink 1.113
  • From David Brown@david.brown@hesbynett.no to comp.lang.misc on Tue Nov 29 17:54:18 2022
    From Newsgroup: comp.lang.misc

    On 29/11/2022 13:49, James Harris wrote:
    On 28/11/2022 16:50, David Brown wrote:
    On 28/11/2022 15:04, Bart wrote:
    On 28/11/2022 10:17, David Brown wrote:
    On 27/11/2022 19:18, Bart wrote:

    ...

    Remember the subject is about /creating/ the allocation library. That
    means being able to specify how it will work. I would rather some of
    that work was offloaded to the allocator, since the author of the
    library may have a better idea of allocation strategies than I do.


    Did you miss the bit where I explained, several times, about how the
    implementation of "realloc" could use the extra space if it happens to
    be enough to satisfy the new request?  In such cases, realloc will be
    extremely fast.

    Could you have holed your own argument below the waterline? You mention realloc being "extremely fast" when it has room to expand so you appear
    to acknowledge that it could be relatively slow when there is not enough room. The problem, I would suggest, is that under your scheme an
    application would not know whether a particular call to realloc was
    going to get the fast or the slow response. Correct?

    Your description is correct, but I don't see the problem. If you need
    the memory, you need the memory - correctness trumps speed. And if you
    need high speed (or as is often the case, predictable time limits), then
    you don't use a general-purpose allocator. You use something like a
    pool of fixed size blocks that matches the application, or some other specialised allocator.


    So the very call which needed a fast response /could not use/ your
    realloc because it would have no way to know whether the allocator's response would be fast or slow.


    If the application needs memory, it needs memory. The situations where
    you can use a tiny bit of extra memory if that is available quickly but
    you'd rather do without than spend time, are almost non-existent, and
    you would have been better off requesting a larger amount in the first
    place.

    It is /wrong/ to design a general-purpose allocator around such a
    hypothetical situation, and in so doing limit the implementation or
    encourage more complex code constructs in user code.

    A far better solution here is to make a simple general-purpose allocator
    that does its job without unnecessary extra interfaces. And make
    efficient string processing part of the language or part of the standard library, using a specialised allocator for the purpose. Even better,
    you should also make pool allocators and/or some kind of abstract
    allocator interface so that allocators can be made available that are
    better optimised for particular use-cases.

    In other words, there may be a fast realloc sitting there available to
    the program but it could not take advantage of it the very kind of call which needs the speed. By contrast, under my proposal a program /could/
    take advantage of the extra speed - and more. See below.

    ...

    That sounds backwards. In what way is it to making assumptions to
    actually ASK what an implementation has reserved?


    You are assuming particular characteristics of the allocator that are
    inappropriate for a general-purpose memory allocator.  All a low-level
    general allocator needs to do is have a function to allocate memory, a
    function to release memory, and optionally (but usefully) a function
    to increase an allocation efficiently.  Every function you include
    beyond that, every detail in specifications, is a limitation on the
    flexibility of the allocator.

    Here's an example.

    Suppose I make an allocator that allocates everything in 128-byte
    blocks, tracked with bitmaps.

    It seems odd to use fixed-size slots for variably sized objects. And 128 bytes is rather coarse but I will, below, apply my proposed scheme to
    your scenario.

    Good modern allocation systems will use different block sizes for
    different purposes. You don't pack everything into the smallest
    possible size - that just leads to heap fragmentation and far more
    wasted space.


    BTW, I can tell you are a professional as you expect us to have the 128-times table in our heads!


    I should have thought that was natural for everyone here :-) Feel free
    to use a calculator.


    You ask for 800 bytes - I give you that by allocating 7 blocks, for a
    total of 896 bytes.  You now want to increase the size of the
    allocation, ideally to 1000 bytes.  You call the "get_real_size"
    function and find there are 96 bytes extra.  What is your code going
    to do now?  Maybe it thinks getting more than 896 bytes is going to be
    slow, and makes do with just 896 bytes.  But that could be wrong -
    perhaps my allocator happens to have the next block free, so
    increasing to 1000 bytes would be almost instant - just a check and an
    OR on a bitmap.  Maybe it thinks it really needs the 1000 bytes, in
    which case the call to "get_real_size" is completely wasted.  The
    information from "get_real_size" is /useless/.  It gives you nothing.

    Under the proposal, if the code had to have the 1000 bytes as a single
    run it would call m_realloc(). That would extend the 896 to 1024 if
    there was space. If not, it would create a new allocation and move the original there. This is the same as C's realloc().

    Yes.


    By contrast, if the code did not require the 1000 bytes to be in a
    single run it would see that the current run was 896 and would call m_resize() to ask whether the run could be extended in place. As before,
    if there was space available the allocator would increase the allocation
    to 1024. If there was not enough space, however, (i.e. if the next block
    was occupied) it would leave the allocation as 896. The caller would
    then create a new allocation for the rest of the space it needed. (The caller would likely also link the two allocations together.)


    That is all just imaginary - real code does not do that. Tracking it
    all at the application side would take a good deal extra code with
    associated bugs (and it is questionable if it is even testable), and
    will probably waste as much time faffing around as it could hope to gain
    in the few cases where a "m_resize" was fast.

    Note that under the proposal a program could still go with the simple realloc if that's what it wanted to do but it would also have other
    options.


    Don't design a language or library based on squeezing in as many
    features as you can imagine possibly being useful on a few occasions to
    a few people. Design it simply and cleanly, in a way that can be
    expanded by users as needed, and where the implementation can be changed.

    I'd start with an object-based interface for an "Allocator", with methods :

    Allocator.alloc(size) -> pointer
    Allocator.alloc_zero(size) -> pointer
    Allocator.realloc(pointer, oldsize, newsize) -> pointer
    Allocator.free(pointer, size)

    (You might dispense with the non-zeroing alloc too.)

    The standard library should provide a standard general-purpose
    Allocator, called "heap". You might also provide other Allocators, such
    as "thread_unsafe_heap" (being thread unsafe, it could be faster - but
    the default heap must be thread safe), "nofree_stack", "big_heap", "small_heap", "binary_pools", and whatever else you feel fits.

    Don't bother making the allocators return null pointers on failure. Programmers don't check their malloc returns well enough as it is, and allocations never fail in PC programming unless the programmer has
    seriously screwed up somewhere. Throw an exception, call an error
    handler, crash the code, or whatever you choose to do normally on a
    critical error.



    --- Synchronet 3.19c-Linux NewsLink 1.113
  • From James Harris@james.harris.1@gmail.com to comp.lang.misc on Tue Nov 29 17:10:45 2022
    From Newsgroup: comp.lang.misc

    On 29/11/2022 16:54, David Brown wrote:
    On 29/11/2022 11:43, James Harris wrote:
    On 28/11/2022 14:04, Bart wrote:
    On 28/11/2022 10:17, David Brown wrote:

    ...

       That would be lying
    to the allocator.  When people start doing that kind of thing, it
    means they are making undocumented assumptions about implementations
    - and that means implementations get stuck with with these
    limitations, or user code breaks.

    I wanted to pick up on David talking about 'lying' to the allocator.
    That's a misleadingly emotive term probably based on the idea that the
    allocation call requests exactly N bytes. Instead, the initial
    request, in this case, would be for "at least N". Asking "how many" is
    not in any way lying.


    Allocation functions like "malloc" do not ask for "at least N bytes".
    They ask for "N bytes".  And the allocator gives you "N bytes" (if it can).  What you get is a pointer and the guarantee that you can use N
    bytes from that pointer.  You don't get to use any more than N bytes,
    even if the allocation results in M extra bytes being left unusable at
    the end of the N bytes.  Using those M extra bytes without telling the allocator (by "realloc") is, if not "lying", at least making unwarranted assumptions and misusing the space behind the back of the allocator that owns the space.

    Programming is all about specifications.  "malloc" is specified to
    return a pointer to N bytes.  That is /all/.  If you start messing
    around by assuming more than that, or trying to sneak out extra
    information, your code is now in the category of "works by luck" instead
    of "works by design".  Lots of people program by luck, but it is hardly something to aspire to or encourage.

    Not sure why you are talking about malloc. I said at the outset that I
    was talking about "an allocator which carries out plain malloc-type allocations (i.e. specified as a number of 8-bit bytes suitably aligned
    for any type) without having to have compatibility with malloc".

    The scheme I put forward was akin to but not the same as the malloc
    approach. Maybe I wasn't clear enough but:

    m_alloc would request AT LEAST N
    m_calloc would request AT LEAST N units of size M and zero them
    m_resize would resize IN PLACE as far as it was able
    m_realloc would resize in place or move existing data to larger area
    m_free would be akin to malloc's free
    m_msize would report the capacity (greater than or equal to N)

    IOW, the request would be "at least N" and m_msize() would report the
    actual allocation.
    --
    James Harris


    --- Synchronet 3.19c-Linux NewsLink 1.113
  • From James Harris@james.harris.1@gmail.com to comp.lang.misc on Tue Nov 29 17:27:47 2022
    From Newsgroup: comp.lang.misc

    On 29/11/2022 16:30, Bart wrote:
    On 29/11/2022 10:43, James Harris wrote:
    On 28/11/2022 14:04, Bart wrote:
    On 28/11/2022 10:17, David Brown wrote:

    ...

    I cannot see any point in first asking how much space I /really/
    have, before asking for the 1020 bytes.  What is the point?

    You're never curious about how much spare capacity was actually set
    aside for your request? You don't think there is any way to
    profitably exploit that?

    It's not about curiosity but about transparency. And greater
    transparency enables greater efficiency.

    It depends on what you intend doing with a function like 'get_real_size'.

    If you want to rely on it for helping implement expanding buffers efficiently, then you need to know that the allocator will give you a worthwhile amount of spare capacity.

    On the contrary, I don't want the allocator to give me spare capacity unnecessarily. Not all allocations will need to expand.


    With a single, general-purpose allocator, there is no reason for it to
    do that; it will only round up enough to meet alignment needs.

    Yes.


    So it might be better to two have kinds of allocator: fixed size, and expanding.

    Absolutely! But if you can only have one type it /has/ to be the latter because only it can cope with both fixed- and variable-sized requests.


    The latter is a bit like your 'at least N' suggestion, but that's not
    strong enough I think. It needs to be specifically suitable for growing.

    Yes, while I was away at the weekend I was thinking about how to do this 'properly'. By that I mean fast, even faster than your
    expanding-allocation example. I came up with something but it's too
    involved to go into here.


    My own allocator was a special-purpose one for an interpreter:
    allocations up to 33MB were rounded up to the next power of two size,
    with a minimum buffer size of 16 bytes.

    I use it from ordinary programs as well, but there it could do with an fixed-size allocator too (which would only round to the next multiple of
    8). There however I probably wouldn't support Free, or not effectively.

    I know power-of-two allocators are popular but I'm not a fan. And
    something still has to cope with a string which begins small but
    incrementally and unpredictably gets larger and larger.
    --
    James Harris


    --- Synchronet 3.19c-Linux NewsLink 1.113
  • From Bart@bc@freeuk.com to comp.lang.misc on Sat Dec 3 13:33:51 2022
    From Newsgroup: comp.lang.misc

    On 29/11/2022 08:10, David Brown wrote:
    On 28/11/2022 20:41, Bart wrote:

    Pointlessly using 20% more memory is fine? Except perhaps where the
    memory is already full.


    You are bike-shedding.  (I hope you know the term - it applies to a lot
    of what you write.)  Concern yourself with things that are important,
    and make a real difference.

    PC ram costs - what, $10 per GB?  That wasted 0.1 GB is a dollar's worth
    of waste.  If a customer has to pay the programmer to improve the
    program to save 20% memory in allocations - how much will /that/ cost?

    You can't install 0.1GB memory, it will have to be in units such as 4GB
    or 8GB, and it may require replacing all the memory if the maximum slots
    have been reached and you need bigger modules.

    Or the memory might not be user-gradable.

    Plus you're thinking in terms of one user of the software: there might
    be 1000 - or 1000000.

    Consider also that machine might already be at near-maximum usage
    because all the 100 running processes have the same attitude as you
    towards memory.


    How much market share will be lost while waiting for the programmer to finish, turning the "good enough for the purpose" program into a
    something that is technically better but /exactly/ the same from the
    users' viewpoint?  What is the cost of the subtle bug in the
    programmer's smarter memory allocator that causes users' data to be corrupted a couple of times a month?

    Add 20% of memory to the 80% of slower runtime. Now each user needs to
    buy a faster machine.



    You worry about utterly irrelevant details of efficiency or speed, yet
    say you consider bugs and incorrect code just a fact of life in
    programming and don't worry about that.  You have the whole thing ass-backwards.

    I do worry about it, but since I'm talking mostly about other people's software, what can /I/ do about that? I see bugs all the time in the
    apps in my gadgets, on websites, in my car's tech, and even on banking applications.

    So building in some sort of fault-tolerance or thinking about what
    customers might do when things when things go, is common sense.

    It might not even be a software bug: someone might yank the power cord.

    Doesn't avionics software have redundant systems so that a fault or
    error in one system is not catastrophic?

    If all software was perfect, every program would be on version 1.000.



    ore about code /correctness/ - avoiding bugs,
    or at least making it easier to find bugs

    You work on bugs of course, but they may need reporting first. But they
    will always be there. They need not be the kind you can detect by
    analysis of a program, but flaws in program logic or in spec, or
    feedback from customers.

    In my commercial apps, commands were implemented with scripts. If a
    script were to go wrong, it would be stopped, but the application
    continued working and they can try something else. If there was an
    actual crash of the main app, they would be able to recover their data
    from auto-save files. This is quite common these days.

    You think apps that have such features (including Thunderbird I'm using
    now) are admitting their failures?

    - and far less about minor
    inefficiencies that do not affect code usability.


    I remember something in Firefox, maybe a bug, where the cap on how many temporary files could be created was ignored. The hard drive always
    seemed very busy when accessing the internet, but it worked, albeit slowly.

    I eventually discovered it had created some 3.5 million temporary files, occupying some 60GB IIRC. Deleting those files from the machine took /14 hours/.

    Some, perhaps including you, would have said 'So what; do you know how
    cheap disk storage is? Buy a bigger drive, buy an SSD'.

    You need to acknowledge when there is a real program, not just blindly
    throw more resources at it.

    --- Synchronet 3.19c-Linux NewsLink 1.113
  • From Dmitry A. Kazakov@mailbox@dmitry-kazakov.de to comp.lang.misc on Sat Dec 3 15:23:00 2022
    From Newsgroup: comp.lang.misc

    On 2022-12-03 14:33, Bart wrote:
    On 29/11/2022 08:10, David Brown wrote:

    You worry about utterly irrelevant details of efficiency or speed, yet
    say you consider bugs and incorrect code just a fact of life in
    programming and don't worry about that.  You have the whole thing
    ass-backwards.

    I do worry about it, but since I'm talking mostly about other people's software, what can /I/ do about that? I see bugs all the time in the
    apps in my gadgets, on websites, in my car's tech, and even on banking applications.

    So building in some sort of fault-tolerance or thinking about what
    customers might do when things when things go, is common sense.

    It might not even be a software bug: someone might yank the power cord.

    Doesn't avionics software have redundant systems so that a fault or
    error in one system is not catastrophic?

    They usually have dynamically allocated memory verboten, so you need not
    to worry about that. (:-))

    BTW, having redundant software is quite stupid idea, since differently
    to hardware software bugs have deterministic nature. No matter how many
    times you repeat an error it is still an error. The other side of the
    coin, once the bug is out, it is out. Hardware can always fail.

    In older days software redundancy was achieved per independent
    development by two independent teams. These days, I believe, it is
    considered too expensive.

    If all software was perfect, every program would be on version 1.000.

    Requirements change, hardware change...

    In my commercial apps, commands were implemented with scripts. If a
    script were to go wrong, it would be stopped, but the application
    continued working and they can try something else.

    Yes, like wiping the log files so that the authority would never find
    out how you killed the client... (:-))
    --
    Regards,
    Dmitry A. Kazakov
    http://www.dmitry-kazakov.de

    --- Synchronet 3.19c-Linux NewsLink 1.113
  • From James Harris@james.harris.1@gmail.com to comp.lang.misc on Sat Dec 3 19:17:19 2022
    From Newsgroup: comp.lang.misc

    On 29/11/2022 15:00, David Brown wrote:
    On 29/11/2022 13:55, James Harris wrote:
    On 29/11/2022 08:10, David Brown wrote:
    On 28/11/2022 20:41, Bart wrote:
    On 28/11/2022 19:01, David Brown wrote:

    ...

    The same applies to memory allocation.  If you are worrying about a >>>>> possible inefficiency of 32 bytes in a memory allocation, you are
    doing things /wrong/.

    If you are not worried about using 32 bytes instead of 16 or 8, then
    /you/ are getting it wrong. Why even bother using a lower level
    language; just use Python.


    Let me say it again.  If you are worried about possible 32 byte
    inefficiencies in a memory allocation, you are doing things /wrong/.

    I cannot speak for Bart but AISI the problem isn't space efficiency
    but time efficiency. If you want to extend a string but there's no
    room to expand it where it is you may prefer to request space for a
    new section of the string rather than to copy all the existing content.


    Pick one or the other - either you want to keep your strings as single contiguous buffers, or you use a chain of buffers.  In the first
    instance, you use realloc and let it copy the data if needed.  In the second case, you use sections of a fixed size and malloc them - you
    don't expand them.

    Ugh! I take it you don't care about performance and scalability!!

    Such designs are just exasperating. Where did you get your idea from? Is
    there some scientific basis for it or did you make it up - perhaps based
    on small cases that are so slow they can be run in batch mode? :-(

    For example, who decided that buffers in a chain couldn't be enlarged?
    Who invented that prohibition and why is it there?


    In either case, messing around with trying to expand only when "get_real_size" indicates it is cheap will lead to more complicated code that is regularly slower, and is only occasionally fast (and the realloc will be fast too in those cases).

    If you need best performance you cannot use realloc; it might move
    what's already there and there's no way before calling it to tell
    whether it will expand in place (fast) or move memory (slow). Hence I
    proposed resize (i.e. 'resize in place') which will never move existing content but would expand the allocation as possible (the new size would
    be visible via the msize call). That's good for not just performance but
    also guarantees that pointers into the buffer won't be invalidated.
    --
    James Harris


    --- Synchronet 3.19c-Linux NewsLink 1.113
  • From James Harris@james.harris.1@gmail.com to comp.lang.misc on Sat Dec 3 19:34:13 2022
    From Newsgroup: comp.lang.misc

    On 03/12/2022 13:33, Bart wrote:
    On 29/11/2022 08:10, David Brown wrote:

    ...

    ore about code /correctness/ - avoiding bugs, or at least making it
    easier to find bugs

    ...

    - and far less about minor
    inefficiencies that do not affect code usability.


    I remember something in Firefox, maybe a bug, where the cap on how many temporary files could be created was ignored. The hard drive always
    seemed very busy when accessing the internet, but it worked, albeit slowly.

    I eventually discovered it had created some 3.5 million temporary files, occupying some 60GB IIRC. Deleting those files from the machine took /14 hours/.

    Great example. I was heartened recently to hear people making
    performance a key design goal. Performance can be just as much a part of engineering a solution as correctness.

    https://en.wikipedia.org/wiki/Data-oriented_design
    https://youtu.be/rX0ItVEVjHc

    A language ought to be able to help a lot with that - e.g. by letting
    the optimiser decide memory layout.

    ...

    You need to acknowledge when there is a real program, not just blindly
    throw more resources at it.

    Indeed! For example, throwing threads and cores at a problem can make it
    run slower rather than faster.
    --
    James Harris


    --- Synchronet 3.19c-Linux NewsLink 1.113
  • From Bart@bc@freeuk.com to comp.lang.misc on Sat Dec 3 20:01:24 2022
    From Newsgroup: comp.lang.misc

    On 03/12/2022 19:34, James Harris wrote:
    On 03/12/2022 13:33, Bart wrote:
    On 29/11/2022 08:10, David Brown wrote:

    ...

    ore about code /correctness/ - avoiding bugs, or at least making it
    easier to find bugs

    ...

    - and far less about minor
    inefficiencies that do not affect code usability.


    I remember something in Firefox, maybe a bug, where the cap on how
    many temporary files could be created was ignored. The hard drive
    always seemed very busy when accessing the internet, but it worked,
    albeit slowly.

    I eventually discovered it had created some 3.5 million temporary
    files, occupying some 60GB IIRC. Deleting those files from the machine
    took /14 hours/.

    Great example. I was heartened recently to hear people making
    performance a key design goal. Performance can be just as much a part of engineering a solution as correctness.

    Here's an interesting discussion I found a link to:

    https://news.ycombinator.com/item?id=24735366

    (I wasn't involved.) It's about fast compilation, and it's refreshing to
    hear people who are enthusiastic about it and can appreciate the benefits.

    (Someone compares 3 seconds build time to 3 minutes. I guess they
    consider 3 seconds as a example of a fast build; I would think that my compiler had hung!)


    --- Synchronet 3.19c-Linux NewsLink 1.113
  • From James Harris@james.harris.1@gmail.com to comp.lang.misc on Sat Dec 3 20:23:15 2022
    From Newsgroup: comp.lang.misc

    On 29/11/2022 16:54, David Brown wrote:
    On 29/11/2022 13:49, James Harris wrote:
    On 28/11/2022 16:50, David Brown wrote:
    On 28/11/2022 15:04, Bart wrote:
    On 28/11/2022 10:17, David Brown wrote:
    On 27/11/2022 19:18, Bart wrote:

    ...

    Remember the subject is about /creating/ the allocation library.
    That means being able to specify how it will work. I would rather
    some of that work was offloaded to the allocator, since the author
    of the library may have a better idea of allocation strategies than
    I do.


    Did you miss the bit where I explained, several times, about how the
    implementation of "realloc" could use the extra space if it happens
    to be enough to satisfy the new request?  In such cases, realloc will
    be extremely fast.

    Could you have holed your own argument below the waterline? You
    mention realloc being "extremely fast" when it has room to expand so
    you appear to acknowledge that it could be relatively slow when there
    is not enough room. The problem, I would suggest, is that under your
    scheme an application would not know whether a particular call to
    realloc was going to get the fast or the slow response. Correct?

    Your description is correct, but I don't see the problem.  If you need
    the memory, you need the memory - correctness trumps speed.  And if you need high speed (or as is often the case, predictable time limits), then
    you don't use a general-purpose allocator.  You use something like a
    pool of fixed size blocks that matches the application, or some other specialised allocator.

    Yes, predictable time limits can matter more than overall run time. If a particular function has to execute quickly then unless it is referring
    to a very small existing buffer it may be unable to call realloc. It
    could, however, call resize to see if the buffer can be resized in place.



    So the very call which needed a fast response /could not use/ your
    realloc because it would have no way to know whether the allocator's
    response would be fast or slow.


    If the application needs memory, it needs memory.  The situations where
    you can use a tiny bit of extra memory if that is available quickly but you'd rather do without than spend time, are almost non-existent, and
    you would have been better off requesting a larger amount in the first place.

    Too many existing programs are stymied by fixed buffer sizes. And it's
    not always possible to decide a buffer size in advance. Sometimes one
    has to read an object into a chain of buffers and then, if desired, consolidate afterwards.


    It is /wrong/ to design a general-purpose allocator around such a hypothetical situation, and in so doing limit the implementation or encourage more complex code constructs in user code.

    A far better solution here is to make a simple general-purpose allocator that does its job without unnecessary extra interfaces.  And make
    efficient string processing part of the language or part of the standard library, using a specialised allocator for the purpose.  Even better,
    you should also make pool allocators and/or some kind of abstract
    allocator interface so that allocators can be made available that are
    better optimised for particular use-cases.

    Well, how would your specialised allocator work for a string of unknown length?

    ...

    Under the proposal, if the code had to have the 1000 bytes as a single
    run it would call m_realloc(). That would extend the 896 to 1024 if
    there was space. If not, it would create a new allocation and move the
    original there. This is the same as C's realloc().

    Yes.


    By contrast, if the code did not require the 1000 bytes to be in a
    single run it would see that the current run was 896 and would call
    m_resize() to ask whether the run could be extended in place. As
    before, if there was space available the allocator would increase the
    allocation to 1024. If there was not enough space, however, (i.e. if
    the next block was occupied) it would leave the allocation as 896. The
    caller would then create a new allocation for the rest of the space it
    needed. (The caller would likely also link the two allocations together.)


    That is all just imaginary - real code does not do that.

    Who says? Just because /you/ wouldn't write code that way does not mean
    that others would not.


    Tracking it
    all at the application side would take a good deal extra code with associated bugs (and it is questionable if it is even testable), and
    will probably waste as much time faffing around as it could hope to gain
    in the few cases where a "m_resize" was fast.

    Let's see how much extra code would be required. Compare these two. Both
    start with an initial buffer pointed to by b.

    Simple:

    if BUF_SIZE < reqd /* If buffer is smaller than we need */
    {
    new_b = alloc(b, BUF_SIZE) /* Create a new buffer */
    .... link into the chain ....
    b = new_b
    }

    Responsive:

    if msize(b) < reqd /* If buffer is smaller than we need */
    {
    resize(b, reqd) /* try to resize in place */
    if msize(b) < reqd /* If buffer is still smaller than we need */
    {
    new_b = alloc(EXTENSION_SIZE) /* Create a new buffer */
    .... link into the chain ....
    b = new_b
    }
    }

    What is that - only 2 extra lines? How can that be overly complicated?


    Note that under the proposal a program could still go with the simple
    realloc if that's what it wanted to do but it would also have other
    options.


    Don't design a language or library based on squeezing in as many
    features as you can imagine possibly being useful on a few occasions to
    a few people.  Design it simply and cleanly, in a way that can be
    expanded by users as needed, and where the implementation can be changed.

    I'd start with an object-based interface for an "Allocator", with methods :

        Allocator.alloc(size) -> pointer
        Allocator.alloc_zero(size) -> pointer
        Allocator.realloc(pointer, oldsize, newsize) -> pointer
        Allocator.free(pointer, size)

    Thanks for coming up with a specific suggestion. What would you want to
    happen if Allocator.free was asked to release a section of memory which
    was wholly within an allocated chunk?


    (You might dispense with the non-zeroing alloc too.)

    The standard library should provide a standard general-purpose
    Allocator, called "heap".  You might also provide other Allocators, such
    as "thread_unsafe_heap" (being thread unsafe, it could be faster - but
    the default heap must be thread safe), "nofree_stack", "big_heap", "small_heap", "binary_pools", and whatever else you feel fits.

    OK. How would they be called

    heap.alloc(size)
    nofree_stack.alloc(size)
    etc

    ?


    Don't bother making the allocators return null pointers on failure. Programmers don't check their malloc returns well enough as it is, and allocations never fail in PC programming unless the programmer has
    seriously screwed up somewhere.  Throw an exception, call an error
    handler, crash the code, or whatever you choose to do normally on a
    critical error.
    Agreed. I throw an exception.
    --
    James Harris


    --- Synchronet 3.19c-Linux NewsLink 1.113
  • From David Brown@david.brown@hesbynett.no to comp.lang.misc on Sun Dec 4 13:31:01 2022
    From Newsgroup: comp.lang.misc

    On 03/12/2022 20:17, James Harris wrote:
    On 29/11/2022 15:00, David Brown wrote:
    On 29/11/2022 13:55, James Harris wrote:
    On 29/11/2022 08:10, David Brown wrote:
    On 28/11/2022 20:41, Bart wrote:
    On 28/11/2022 19:01, David Brown wrote:

    ...

    The same applies to memory allocation.  If you are worrying about >>>>>> a possible inefficiency of 32 bytes in a memory allocation, you
    are doing things /wrong/.

    If you are not worried about using 32 bytes instead of 16 or 8,
    then /you/ are getting it wrong. Why even bother using a lower
    level language; just use Python.


    Let me say it again.  If you are worried about possible 32 byte
    inefficiencies in a memory allocation, you are doing things /wrong/.

    I cannot speak for Bart but AISI the problem isn't space efficiency
    but time efficiency. If you want to extend a string but there's no
    room to expand it where it is you may prefer to request space for a
    new section of the string rather than to copy all the existing content.


    Pick one or the other - either you want to keep your strings as single
    contiguous buffers, or you use a chain of buffers.  In the first
    instance, you use realloc and let it copy the data if needed.  In the
    second case, you use sections of a fixed size and malloc them - you
    don't expand them.

    Ugh! I take it you don't care about performance and scalability!!

    Such designs are just exasperating. Where did you get your idea from? Is there some scientific basis for it or did you make it up - perhaps based
    on small cases that are so slow they can be run in batch mode? :-(

    For example, who decided that buffers in a chain couldn't be enlarged?
    Who invented that prohibition and why is it there?


    What /are/ you talking about?

    Look, at the lowest level, buffers are /never/ enlarged. You've
    allocated a bit of memory, and you use that. If that's not enough, you
    have to allocate a /new/ bit of memory. All this piddle about "real" allocation sizes is just peanuts - a byte here, a byte there, almost irrelevant in the average case and completely irrelevant in the worst case.

    Your choices are whether to allocate your new bit of memory for the new
    data only and chain it together with the first lump, or to allocate
    something big enough for all existing data as well as the new bit, copy
    over the old data and thus have a single contiguous buffer. That second solution gives faster and more convenient access to the data, but costs
    more for the allocation and copying. Trade-offs, as always.




    In either case, messing around with trying to expand only when
    "get_real_size" indicates it is cheap will lead to more complicated
    code that is regularly slower, and is only occasionally fast (and the
    realloc will be fast too in those cases).

    If you need best performance you cannot use realloc; it might move
    what's already there and there's no way before calling it to tell
    whether it will expand in place (fast) or move memory (slow). Hence I proposed resize (i.e. 'resize in place') which will never move existing content but would expand the allocation as possible (the new size would
    be visible via the msize call). That's good for not just performance but also guarantees that pointers into the buffer won't be invalidated.



    You are trying to solve the wrong problem in the wrong place.

    Any information about buffer sizes compared to buffer usage belongs in
    the application code or run-time library layers, not the allocation
    system at the low level. For common functions, such as string handling,
    the run-time library is appropriate for the convenience of users. (The run-time library has the advantage of knowing about the low-level implementation, and thus every allocation it makes can be of a "perfect"
    size without wasted space.)




    --- Synchronet 3.19c-Linux NewsLink 1.113
  • From David Brown@david.brown@hesbynett.no to comp.lang.misc on Sun Dec 4 14:27:20 2022
    From Newsgroup: comp.lang.misc

    On 03/12/2022 21:23, James Harris wrote:
    On 29/11/2022 16:54, David Brown wrote:

    I'm skipping the rest here - either you understand, appreciate and agree
    with what I wrote, or you don't. (That means you either don't
    understand it, don't appreciate it, or just don't agree with it - not necessarily that I don't think you understand it.) That's fair enough -
    it's your language and libraries, not mine.


    Don't design a language or library based on squeezing in as many
    features as you can imagine possibly being useful on a few occasions
    to a few people.  Design it simply and cleanly, in a way that can be
    expanded by users as needed, and where the implementation can be changed.

    I'd start with an object-based interface for an "Allocator", with
    methods :

         Allocator.alloc(size) -> pointer
         Allocator.alloc_zero(size) -> pointer
         Allocator.realloc(pointer, oldsize, newsize) -> pointer
         Allocator.free(pointer, size)

    Thanks for coming up with a specific suggestion. What would you want to happen if Allocator.free was asked to release a section of memory which
    was wholly within an allocated chunk?


    You are asking what the allocator should do if the programmer using it
    writes nonsense? Who cares? Launch the nasal daemons. (You are
    welcome, even strongly recommended, to have debug modes or optional
    run-time checks where efficiency takes a back seat to ease of debugging.
    In such situations the application code is supposed to give the proper values, but the allocator has also tracked the sizes despite the cost -
    then you have a run-time check and fire off error messages.)


    (You might dispense with the non-zeroing alloc too.)

    The standard library should provide a standard general-purpose
    Allocator, called "heap".  You might also provide other Allocators,
    such as "thread_unsafe_heap" (being thread unsafe, it could be faster
    - but the default heap must be thread safe), "nofree_stack",
    "big_heap", "small_heap", "binary_pools", and whatever else you feel
    fits.

    OK. How would they be called

      heap.alloc(size)
      nofree_stack.alloc(size)
      etc

    ?

    Yes - although I'd recommend keeping track of the returned pointers!

    You may also provide some kind of wrapper for objects with constructors
    and destructors (like "new" and "delete" mechanisms found in many
    languages), depending on the rest of the language.



    Don't bother making the allocators return null pointers on failure.
    Programmers don't check their malloc returns well enough as it is, and
    allocations never fail in PC programming unless the programmer has
    seriously screwed up somewhere.  Throw an exception, call an error
    handler, crash the code, or whatever you choose to do normally on a
    critical error.
    Agreed. I throw an exception.




    --- Synchronet 3.19c-Linux NewsLink 1.113
  • From James Harris@james.harris.1@gmail.com to comp.lang.misc on Sun Dec 4 13:54:59 2022
    From Newsgroup: comp.lang.misc

    On 04/12/2022 12:31, David Brown wrote:
    On 03/12/2022 20:17, James Harris wrote:
    On 29/11/2022 15:00, David Brown wrote:
    On 29/11/2022 13:55, James Harris wrote:
    On 29/11/2022 08:10, David Brown wrote:
    On 28/11/2022 20:41, Bart wrote:
    On 28/11/2022 19:01, David Brown wrote:

    ...

    The same applies to memory allocation.  If you are worrying about >>>>>>> a possible inefficiency of 32 bytes in a memory allocation, you >>>>>>> are doing things /wrong/.

    If you are not worried about using 32 bytes instead of 16 or 8,
    then /you/ are getting it wrong. Why even bother using a lower
    level language; just use Python.


    Let me say it again.  If you are worried about possible 32 byte
    inefficiencies in a memory allocation, you are doing things /wrong/.

    I cannot speak for Bart but AISI the problem isn't space efficiency
    but time efficiency. If you want to extend a string but there's no
    room to expand it where it is you may prefer to request space for a
    new section of the string rather than to copy all the existing content. >>>>

    Pick one or the other - either you want to keep your strings as
    single contiguous buffers, or you use a chain of buffers.  In the
    first instance, you use realloc and let it copy the data if needed.
    In the second case, you use sections of a fixed size and malloc them
    - you don't expand them.

    Ugh! I take it you don't care about performance and scalability!!

    Such designs are just exasperating. Where did you get your idea from?
    Is there some scientific basis for it or did you make it up - perhaps
    based on small cases that are so slow they can be run in batch mode? :-(

    For example, who decided that buffers in a chain couldn't be enlarged?
    Who invented that prohibition and why is it there?


    What /are/ you talking about?

    I'm talking about engineering, not the oversimplistic theory, idealism
    and naivety that you keep telling me is all that anyone should ever
    need. :-(


    Look, at the lowest level, buffers are /never/ enlarged.

    Rubbish! Just because /you/ never want such a thing does not mean that
    others do not want it, nor does your dislike for it or your lack of
    experience of it mean that it's a bad idea.

    You've
    allocated a bit of memory, and you use that.  If that's not enough, you have to allocate a /new/ bit of memory.  All this piddle about "real" allocation sizes is just peanuts - a byte here, a byte there, almost irrelevant in the average case and completely irrelevant in the worst case.

    That may be what /you/ would always do but your view shouldn't restrict others. In fact, you talk about saving only 'a byte here, a byte there'.
    That can make a difference to some applications.

    For example, say a string library implements two formats: a fast format
    for short strings and a general format for others. (I know of at least
    two libraries which do.) Having a single extra byte in the short format
    allows more strings to use the fast form, for a net gain.

    So even a single byte can make a difference. But because of alignment in
    what we have been talking about a programmer could gain 3, 7 or 15 bytes
    in every allocation which he would not otherwise have had! These extra
    bytes would be completely free and would otherwise have been wasted.
    There is no good reason to prevent a client from using them.


    Your choices are whether to allocate your new bit of memory for the new
    data only and chain it together with the first lump, or to allocate something big enough for all existing data as well as the new bit, copy
    over the old data and thus have a single contiguous buffer.  That second solution gives faster and more convenient access to the data, but costs
    more for the allocation and copying.  Trade-offs, as always.

    Whether to chain or to reallocate is an interesting consideration but it
    is orthogonal to the basic allocation. If chaining, finding out how much
    space is available can avoid forming a new link. If reallocating,
    finding out how much space is available can avoid carrying out a costly reallocation.

    On the chain/reallocate decision ... in simple terms, if I knew the
    length of the string I was creating I would use a single allocation
    whereas if I did not then I would use chaining.





    In either case, messing around with trying to expand only when
    "get_real_size" indicates it is cheap will lead to more complicated
    code that is regularly slower, and is only occasionally fast (and the
    realloc will be fast too in those cases).

    If you need best performance you cannot use realloc; it might move
    what's already there and there's no way before calling it to tell
    whether it will expand in place (fast) or move memory (slow). Hence I
    proposed resize (i.e. 'resize in place') which will never move
    existing content but would expand the allocation as possible (the new
    size would be visible via the msize call). That's good for not just
    performance but also guarantees that pointers into the buffer won't be
    invalidated.



    You are trying to solve the wrong problem in the wrong place.

    Any information about buffer sizes compared to buffer usage belongs in
    the application code or run-time library layers, not the allocation
    system at the low level.  For common functions, such as string handling, the run-time library is appropriate for the convenience of users.  (The run-time library has the advantage of knowing about the low-level implementation, and thus every allocation it makes can be of a "perfect" size without wasted space.)
    Well, the software would be in layers.

    app
    |
    V
    string library
    |
    V
    allocator

    What I've proposed in this thread is a simple and small change to the allocator which would allow the string library to make best use of the
    spaces which the allocator returns to the string library.

    I am not sure why you keep objecting to that but I am pretty sure you
    and I aren't going to agree on this stuff. Still, no problem. Agreement
    is not mandatory. :-)
    --
    James Harris


    --- Synchronet 3.19c-Linux NewsLink 1.113
  • From James Harris@james.harris.1@gmail.com to comp.lang.misc on Sun Dec 4 14:07:17 2022
    From Newsgroup: comp.lang.misc

    On 04/12/2022 13:27, David Brown wrote:
    On 03/12/2022 21:23, James Harris wrote:
    On 29/11/2022 16:54, David Brown wrote:

    I'm skipping the rest here - either you understand, appreciate and agree with what I wrote, or you don't.  (That means you either don't
    understand it, don't appreciate it, or just don't agree with it - not necessarily that I don't think you understand it.)  That's fair enough - it's your language and libraries, not mine.

    :-) I just wrote something similar to you in another reply.



    Don't design a language or library based on squeezing in as many
    features as you can imagine possibly being useful on a few occasions
    to a few people.  Design it simply and cleanly, in a way that can be
    expanded by users as needed, and where the implementation can be
    changed.

    I'd start with an object-based interface for an "Allocator", with
    methods :

         Allocator.alloc(size) -> pointer
         Allocator.alloc_zero(size) -> pointer
         Allocator.realloc(pointer, oldsize, newsize) -> pointer
         Allocator.free(pointer, size)

    Thanks for coming up with a specific suggestion. What would you want
    to happen if Allocator.free was asked to release a section of memory
    which was wholly within an allocated chunk?


    You are asking what the allocator should do if the programmer using it writes nonsense?

    No, I was asking whether you intended that a programmer should be able
    to release memory in units other than those he used for the allocation requests. For example, consider an allocation from A to D when the
    programmer wants to release B to C within it. A < B < C < D:

    ----------------------------------------------------
    A B C D

    I was asking whether you thought the programmer should be able to
    release B to C before releasing the other parts.

    I thought that was what you and Bart (who IIRC also mentioned something similar) had in mind and I spend some time designing an allocator to
    support splitting memory in that way down to the byte level. It was an interesting exercise and I may use it one day but I thought you had some
    use cases in mind.

    Who cares?  Launch the nasal daemons.

    Never, never, and never!

    ...
    --
    James Harris


    --- Synchronet 3.19c-Linux NewsLink 1.113
  • From Bart@bc@freeuk.com to comp.lang.misc on Sun Dec 4 14:47:03 2022
    From Newsgroup: comp.lang.misc

    On 04/12/2022 14:07, James Harris wrote:
    On 04/12/2022 13:27, David Brown wrote:
    On 03/12/2022 21:23, James Harris wrote:
    On 29/11/2022 16:54, David Brown wrote:

    I'm skipping the rest here - either you understand, appreciate and
    agree with what I wrote, or you don't.  (That means you either don't
    understand it, don't appreciate it, or just don't agree with it - not
    necessarily that I don't think you understand it.)  That's fair enough
    - it's your language and libraries, not mine.

    :-) I just wrote something similar to you in another reply.



    Don't design a language or library based on squeezing in as many
    features as you can imagine possibly being useful on a few occasions
    to a few people.  Design it simply and cleanly, in a way that can be >>>> expanded by users as needed, and where the implementation can be
    changed.

    I'd start with an object-based interface for an "Allocator", with
    methods :

         Allocator.alloc(size) -> pointer
         Allocator.alloc_zero(size) -> pointer
         Allocator.realloc(pointer, oldsize, newsize) -> pointer
         Allocator.free(pointer, size)

    Thanks for coming up with a specific suggestion. What would you want
    to happen if Allocator.free was asked to release a section of memory
    which was wholly within an allocated chunk?


    You are asking what the allocator should do if the programmer using it
    writes nonsense?

    No, I was asking whether you intended that a programmer should be able
    to release memory in units other than those he used for the allocation requests. For example, consider an allocation from A to D when the programmer wants to release B to C within it. A < B < C < D:

      ----------------------------------------------------
      A        B   C                                      D

    I was asking whether you thought the programmer should be able to
    release B to C before releasing the other parts.

    I thought that was what you and Bart (who IIRC also mentioned something similar)

    I don't think so. You suggestion sounds a little crazy actually: someone allocates a buffer to store a string, which spans A to D exclusive, and
    which will have ensured that A was aligned, and D was aligned (for the
    next alloc)

    Later, they want to independently free a section of that from B to C exclusive, to turn that one block into three:

    A -> B allocated
    B -> C free
    C -> D allocated

    So what happens to the alignment needs of B and C? What happens to the
    string that was stored in it, of which 2 remnants remain, and one chunk
    is missing?

    Presumably the process can be repeated, so that any block will be full
    of freed holes, which will eventually join up, but will now be fragmented.


    had in mind and I spend some time designing an allocator to
    support splitting memory in that way down to the byte level. It was an interesting exercise and I may use it one day but I thought you had some
    use cases in mind.

    The only use case I can think of is within the allocator, splitting up
    an available free block into smaller ones, but noting alignment needs
    and minimum sizes (like 8 bytes).

    Is this supposed to be some generalisation of Realloc? Currently such a routine working on A->D would turn it into this when reducing:

    A->B still allocated
    B->D now available

    or into this when expanding:

    A->E now allocated including D->E

    Here E>D, but the whole block may need relocating in either case.


    --- Synchronet 3.19c-Linux NewsLink 1.113
  • From James Harris@james.harris.1@gmail.com to comp.lang.misc on Sun Dec 4 15:56:51 2022
    From Newsgroup: comp.lang.misc

    On 04/12/2022 14:47, Bart wrote:
    On 04/12/2022 14:07, James Harris wrote:
    On 04/12/2022 13:27, David Brown wrote:

    ...

    You are asking what the allocator should do if the programmer using
    it writes nonsense?

    No, I was asking whether you intended that a programmer should be able
    to release memory in units other than those he used for the allocation
    requests. For example, consider an allocation from A to D when the
    programmer wants to release B to C within it. A < B < C < D:

       ----------------------------------------------------
       A        B   C                                      D

    I was asking whether you thought the programmer should be able to
    release B to C before releasing the other parts.

    I thought that was what you and Bart (who IIRC also mentioned
    something similar)

    I don't think so. You suggestion sounds a little crazy actually: someone allocates a buffer to store a string, which spans A to D exclusive, and which will have ensured that A was aligned, and D was aligned (for the
    next alloc)

    In the ABCD example I wasn't talking about strings.


    Later, they want to independently free a section of that from B to C exclusive, to turn that one block into three:

       A -> B  allocated
       B -> C  free
       C -> D  allocated

    So what happens to the alignment needs of B and C? What happens to the string that was stored in it, of which 2 remnants remain, and one chunk
    is missing?

    Again, I wasn't talking about strings but a general case. I thought it
    was you who, along with David, suggested a 'free' which included the
    length as in

    free(address, length)

    It got me thinking that the system call munmap does similar: calls to it supply the length as well as the starting address. The call unmaps pages
    and, AIUI, a run of pages being freed doesn't have to match the run
    allocated. IOW it supports freeing B to C from the illustration, above,
    (and other permutations) as long as we understand that the amount freed
    will be whole pages. If that's usefully done for mmap then I was
    thinking that perhaps it could be useful be done for other allocators,
    and that maybe that was what you had in mind.


    Presumably the process can be repeated, so that any block will be full
    of freed holes, which will eventually join up, but will now be fragmented.

    I would merge adjacent holes into one.



    had in mind and I spend some time designing an allocator to support
    splitting memory in that way down to the byte level. It was an
    interesting exercise and I may use it one day but I thought you had
    some use cases in mind.

    The only use case I can think of is within the allocator, splitting up
    an available free block into smaller ones, but noting alignment needs
    and minimum sizes (like 8 bytes).

    Is this supposed to be some generalisation of Realloc? Currently such a routine working on A->D would turn it into this when reducing:

       A->B still allocated
       B->D now available

    No, it's nothing to do with realloc.

    If A-B-C-D was allocated (as a single range A-D) and then B-C was freed
    the result would be what you pointed out earlier

    A-B still allocated
    B-C freed
    C-D still allocated


    or into this when expanding:

       A->E now allocated including D->E

    Here E>D, but the whole block may need relocating in either case.

    I'm not sure what that means.

    I decided to make each split (the start/end of each piece of allocated
    or unallocated memory) a byte address so that user code could call
    allocate and free with arbitrary values and place a split at literally
    any address. In the ABCD example, the actual addresses could be, say,

    A = 2 (3 bytes allocated at addresses 2, 3 and 4)
    B = 5 (1 byte unallocated)
    C = 6 (2 bytes allocated)
    D = 8 (the potential start of a new allocation)

    For each range a bit would be needed to indicate free/allocated. Because allocations would have to be byte aligned I chose the top bit, and to separately manage memory where the top address bit was 0 and the other
    where the top bit was 1.

    Then with all allocation markers being a single machine word I would
    store them as 'keys without values' in an in-memory B tree or B+ tree.
    (There would be one B tree or B+ tree for each half of the address space.)

    As I say, I thought that you and/or David had a use case for arbitrary freeing, but it was an interesting thought experiment nonetheless.
    --
    James Harris


    --- Synchronet 3.19c-Linux NewsLink 1.113
  • From David Brown@david.brown@hesbynett.no to comp.lang.misc on Sun Dec 4 17:30:02 2022
    From Newsgroup: comp.lang.misc

    On 03/12/2022 20:34, James Harris wrote:
    On 03/12/2022 13:33, Bart wrote:

    I eventually discovered it had created some 3.5 million temporary
    files, occupying some 60GB IIRC. Deleting those files from the machine
    took /14 hours/.

    Somebody is using an OS with a crappy filesystem - combined with a
    crappy treatment of temporary files.

    I'm not sure, however, that the existence of bugs or poor design in
    /massive/ programs like Firefox mean it's okay to be happy with bugs in
    your own code.


    Great example. I was heartened recently to hear people making
    performance a key design goal. Performance can be just as much a part of engineering a solution as correctness.
    If your code is not correct, it doesn't matter how quickly you get the
    wrong answer.

    Performance might be /part/ of correctness - the specification for the
    program or function might say "do this calculation within time X" or
    include limits for memory usage. In that case, a function that takes
    too long or too much memory is not a correct solution. But other than
    such cases, correctness /always/ trumps performance.

    (Of course, lots of code is so badly specified that it's hard to say
    what a "correct" implementation should actually do.)

    And remember, 90% of time is spend in 10% of the code, meaning that performance is of little relevance for almost all programming (but it is
    very important in a small proportion of code). Correctness, however, is important in 100% of code.


    It is easier to make correct code fast than to make fast code correct.
    It is easier to make clear code correct than to make correct code clear.
    Write you code /clearly/ with an aim for /correctness/ - don't focus
    on performance.


    --- Synchronet 3.19c-Linux NewsLink 1.113
  • From Bart@bc@freeuk.com to comp.lang.misc on Sun Dec 4 17:46:47 2022
    From Newsgroup: comp.lang.misc

    On 04/12/2022 16:30, David Brown wrote:
    On 03/12/2022 20:34, James Harris wrote:
    On 03/12/2022 13:33, Bart wrote:

    I eventually discovered it had created some 3.5 million temporary
    files, occupying some 60GB IIRC. Deleting those files from the
    machine took /14 hours/.

    Somebody is using an OS with a crappy filesystem - combined with a
    crappy treatment of temporary files.

    That's what I thought. Until I worked out that deleting 3.5M files in 14
    hours was still at a rate of 70 file deletions per second. That is not
    that terrible, given that each file is within a single folder of
    millions of files (I don't know the complexities of NTFS or the
    mechanics involved).

    No doubt a Linux system would have handled that in - I don't know, how
    long /would/ it have taken on a spinning hard drive?



    --- Synchronet 3.19c-Linux NewsLink 1.113
  • From James Harris@james.harris.1@gmail.com to comp.lang.misc on Sun Dec 4 18:21:04 2022
    From Newsgroup: comp.lang.misc

    On 04/12/2022 16:30, David Brown wrote:
    On 03/12/2022 20:34, James Harris wrote:

    ...

    Performance can be just as much a part
    of engineering a solution as correctness.

    ... (snipped comments which basically expanded on the above)


    It is easier to make correct code fast than to make fast code correct.
    It is easier to make clear code correct than to make correct code clear.
     Write you code /clearly/ with an aim for /correctness/ - don't focus
    on performance.

    I understand where you are coming from but I have to say I think you are
    wrong in keeping downplaying performance. As I said, it /can be/ just as important as correctness in engineering a solution. And it might not be.
    There are different scenarios:

    If you are writing an application and you know that performance of it is
    not critical (and it contains no time-critical parts) then by all means
    focus laser-like on correctness.

    By contrast, if you are writing an application which has time-critical
    parts then you have to ensure time compliance. This case, like the first
    one, is simple.

    But there are areas of middle ground in which performance requirements
    are not so straightforward. Say you are writing an OS, specifying a
    language, or designing a library. You don't know in advance what will be
    the timing requirements of the apps that will use them. Nor do you know
    what crazy ways apps may use your OS, language or library. You have to anticipate users going off piste and stressing the design with larger
    elements and more of them than you would have thought possible.

    The case in point is two libraries: a memory allocator and a string
    library which uses the memory allocator. What I am saying to you is that
    such libraries need to be correct but they also need to scale to large strings, many strings, many string operations, many other non-string allocation activity, etc, because it's impossible to say just now how
    they will be used. It's no good to just "focus on correctness".
    --
    James Harris


    --- Synchronet 3.19c-Linux NewsLink 1.113
  • From James Harris@james.harris.1@gmail.com to comp.lang.misc on Sun Dec 4 21:20:11 2022
    From Newsgroup: comp.lang.misc

    On 04/12/2022 17:46, Bart wrote:
    On 04/12/2022 16:30, David Brown wrote:
    On 03/12/2022 20:34, James Harris wrote:
    On 03/12/2022 13:33, Bart wrote:

    I eventually discovered it had created some 3.5 million temporary
    files, occupying some 60GB IIRC. Deleting those files from the
    machine took /14 hours/.

    Somebody is using an OS with a crappy filesystem - combined with a
    crappy treatment of temporary files.

    That's what I thought. Until I worked out that deleting 3.5M files in 14 hours was still at a rate of 70 file deletions per second. That is not
    that terrible, given that each file is within a single folder of
    millions of files (I don't know the complexities of NTFS or the
    mechanics involved).

    No doubt a Linux system would have handled that in - I don't know, how
    long /would/ it have taken on a spinning hard drive?


    I didn't try the full 3.5 million but took these timings on a Linux ext4:

    creation of 350,000 empty files: 527/s
    deletion of 350,000 empty files: 178/s
    creation of 35,000 empty files: 172/s
    deletion of 35,000 empty files: 179/s

    I suspect that the initially greater speed may have been something to do
    with the directory starting off as empty which was not the case later.
    Even after deleting all files in the folder the directory still occupies 12Mby.

    For ref, commands used for 350,000:

    time for (( i = 0; i < 350000; i++ )) ; do touch file$i.trial; done
    time for (( i = 0; i < 350000; i++ )) ; do rm file$i.trial; done
    --
    James Harris


    --- Synchronet 3.19c-Linux NewsLink 1.113
  • From David Brown@david.brown@hesbynett.no to comp.lang.misc on Sun Dec 4 23:23:07 2022
    From Newsgroup: comp.lang.misc

    On 04/12/2022 14:54, James Harris wrote:
    On 04/12/2022 12:31, David Brown wrote:
    On 03/12/2022 20:17, James Harris wrote:
    On 29/11/2022 15:00, David Brown wrote:
    On 29/11/2022 13:55, James Harris wrote:
    On 29/11/2022 08:10, David Brown wrote:
    On 28/11/2022 20:41, Bart wrote:
    On 28/11/2022 19:01, David Brown wrote:

    ...

    The same applies to memory allocation.  If you are worrying
    about a possible inefficiency of 32 bytes in a memory
    allocation, you are doing things /wrong/.

    If you are not worried about using 32 bytes instead of 16 or 8, >>>>>>> then /you/ are getting it wrong. Why even bother using a lower
    level language; just use Python.


    Let me say it again.  If you are worried about possible 32 byte
    inefficiencies in a memory allocation, you are doing things /wrong/. >>>>>
    I cannot speak for Bart but AISI the problem isn't space efficiency >>>>> but time efficiency. If you want to extend a string but there's no
    room to expand it where it is you may prefer to request space for a >>>>> new section of the string rather than to copy all the existing
    content.


    Pick one or the other - either you want to keep your strings as
    single contiguous buffers, or you use a chain of buffers.  In the
    first instance, you use realloc and let it copy the data if needed.
    In the second case, you use sections of a fixed size and malloc them
    - you don't expand them.

    Ugh! I take it you don't care about performance and scalability!!

    Such designs are just exasperating. Where did you get your idea from?
    Is there some scientific basis for it or did you make it up - perhaps
    based on small cases that are so slow they can be run in batch mode? :-( >>>
    For example, who decided that buffers in a chain couldn't be
    enlarged? Who invented that prohibition and why is it there?


    What /are/ you talking about?

    I'm talking about engineering, not the oversimplistic theory, idealism
    and naivety that you keep telling me is all that anyone should ever
    need. :-(


    I am an engineer, not an academic. (I just happen to have a more
    academic background, and more academic interest, than most software engineers.) But like any good engineer, I want the basis of my work to
    be solidly founded on good theory. An mechanical engineer doesn't have
    to understand why springs work linearly or how moments of inertia
    formula are derived - but they need to know that the people who worked
    out these things, did so correctly. A typical software engineer does
    not need to know how to design programming languages, or how to make a
    mutex or thread-safe queue. But they /do/ have to rely on the people
    who made these lower-level parts and that /they/ understand the theory
    and can be sure it is correct.

    If people cannot rely on your language and libraries to be correct -
    /always/ correct, in theory and in practice - then the whole exercise is
    just a fun game to play on your own.

    Some people complain about languages like C and C++, saying it should be
    easy to add some new feature, or they don't see why it has particular
    quirks. The answer often comes down to the immense amount of work that
    goes into the theory - the analysis of the implications of changes, and
    how it might affect other aspects of the language or existing code no
    matter how hypothetical or unusual.

    If you haven't understood the simplistic theories, you can't begin to
    work on the more advanced theories - and certainly not on anything
    practical in a language. Remember, /you/ are designing the fundaments
    that others will build on. If those are not solid and correct, things
    will break.


    Look, at the lowest level, buffers are /never/ enlarged.

    Rubbish! Just because /you/ never want such a thing does not mean that others do not want it, nor does your dislike for it or your lack of experience of it mean that it's a bad idea.


    They are /not/ enlarged.

    You allocate some memory for a buffer. You allocate more memory later
    for something else - it gets put after the first buffer. How are you
    going to enlarge the first buffer? Stomp all over the second allocation?

    What happens is that /new/ memory is allocated. At a higher level, this
    can look like a buffer enlargement when someone calls "realloc" with a
    larger value. But not at the low level, the implementation - the buffer
    is not enlarged.

    You've allocated a bit of memory, and you use that.  If that's not
    enough, you have to allocate a /new/ bit of memory.  All this piddle
    about "real" allocation sizes is just peanuts - a byte here, a byte
    there, almost irrelevant in the average case and completely irrelevant
    in the worst case.

    That may be what /you/ would always do but your view shouldn't restrict others. In fact, you talk about saving only 'a byte here, a byte there'. That can make a difference to some applications.


    As I have said before, if it makes a difference, you are doing things
    wrong. You are using the wrong allocator, or the wrong implementation,
    or the wrong algorithm, or putting the size tracking in the wrong place,
    or using the wrong type of processor.

    For example, say a string library implements two formats: a fast format
    for short strings and a general format for others. (I know of at least
    two libraries which do.) Having a single extra byte in the short format allows more strings to use the fast form, for a net gain.

    So even a single byte can make a difference. But because of alignment in what we have been talking about a programmer could gain 3, 7 or 15 bytes
    in every allocation which he would not otherwise have had! These extra
    bytes would be completely free and would otherwise have been wasted.
    There is no good reason to prevent a client from using them.


    It is quite common to have string formats that support arbitrarily long strings with dynamically allocated memory (either as a contiguous
    buffer, or as a chain), and have a "short string optimisation" for
    holding data locally. You do not allocate data for the short string,
    and you do not increase its buffer size, or faff around trying to get a
    byte or two extra. You make a fixed format with a fixed size
    short-string buffer - if you need more, you are now a "long" string.
    The size of the short-string buffer is fixed in the type (possibly
    always the same, possibly as a template parameter).

    If the string type is part of the language's basic standard library (and
    I'd recommend that), then the buffer size is going to be picked to match
    the allocation size from the standard library allocator used. It would
    be downright idiotic to have an allocator that, say, rounds sizes up to 32-byte blocks and then have a string structure that was 20 bytes long, supporting short strings of up to 18 bytes. It would be even dafter to
    have a string structure that is of varying size and have users
    re-allocate it to grow it to use the extra size that the library (not
    the user) knows is always there.


    Your choices are whether to allocate your new bit of memory for the
    new data only and chain it together with the first lump, or to
    allocate something big enough for all existing data as well as the new
    bit, copy over the old data and thus have a single contiguous buffer.
    That second solution gives faster and more convenient access to the
    data, but costs more for the allocation and copying.  Trade-offs, as
    always.

    Whether to chain or to reallocate is an interesting consideration but it
    is orthogonal to the basic allocation. If chaining, finding out how much space is available can avoid forming a new link. If reallocating,
    finding out how much space is available can avoid carrying out a costly reallocation.

    On the chain/reallocate decision ... in simple terms, if I knew the
    length of the string I was creating I would use a single allocation
    whereas if I did not then I would use chaining.


    /You/ are making the string type and library. You will never know what
    the user wants (unless you are making a one-man language where you
    change the language and the compiler as often as you write programs in
    the language). You will /never/ make decisions that are ideal for
    everyone. You pick a balance between making general choices that are
    good enough for a wide range of uses, or making the support to let users design these types for themselves.





    In either case, messing around with trying to expand only when
    "get_real_size" indicates it is cheap will lead to more complicated
    code that is regularly slower, and is only occasionally fast (and
    the realloc will be fast too in those cases).

    If you need best performance you cannot use realloc; it might move
    what's already there and there's no way before calling it to tell
    whether it will expand in place (fast) or move memory (slow). Hence I
    proposed resize (i.e. 'resize in place') which will never move
    existing content but would expand the allocation as possible (the new
    size would be visible via the msize call). That's good for not just
    performance but also guarantees that pointers into the buffer won't
    be invalidated.



    You are trying to solve the wrong problem in the wrong place.

    Any information about buffer sizes compared to buffer usage belongs in
    the application code or run-time library layers, not the allocation
    system at the low level.  For common functions, such as string
    handling, the run-time library is appropriate for the convenience of
    users.  (The run-time library has the advantage of knowing about the
    low-level implementation, and thus every allocation it makes can be of
    a "perfect" size without wasted space.)
    Well, the software would be in layers.

         app
          |
          V
     string library
          |
          V
      allocator


    Yes.

    What I've proposed in this thread is a simple and small change to the allocator which would allow the string library to make best use of the spaces which the allocator returns to the string library.


    No.

    The simple method is that the string library knows the size of chunks
    provided by the allocator, and always uses that information.

    I am not sure why you keep objecting to that but I am pretty sure you
    and I aren't going to agree on this stuff. Still, no problem. Agreement
    is not mandatory. :-)


    It would be boring indeed if we all agreed! /Disagreement/ is mandatory
    for an interesting thread :-)

    But we do agree on many things.



    --- Synchronet 3.19c-Linux NewsLink 1.113
  • From David Brown@david.brown@hesbynett.no to comp.lang.misc on Sun Dec 4 23:44:40 2022
    From Newsgroup: comp.lang.misc

    On 04/12/2022 18:46, Bart wrote:
    On 04/12/2022 16:30, David Brown wrote:
    On 03/12/2022 20:34, James Harris wrote:
    On 03/12/2022 13:33, Bart wrote:

    I eventually discovered it had created some 3.5 million temporary
    files, occupying some 60GB IIRC. Deleting those files from the
    machine took /14 hours/.

    Somebody is using an OS with a crappy filesystem - combined with a
    crappy treatment of temporary files.

    That's what I thought. Until I worked out that deleting 3.5M files in 14 hours was still at a rate of 70 file deletions per second. That is not
    that terrible, given that each file is within a single folder of
    millions of files (I don't know the complexities of NTFS or the
    mechanics involved).

    No doubt a Linux system would have handled that in - I don't know, how
    long /would/ it have taken on a spinning hard drive?


    The key to doing that kind of operation efficiently is to buffer them.
    Windows is too enthusiastic about committing everything to disk at every point. So for each file, it has to find it in the directory (which is a
    big effort at the best of times - but made /vastly/ worse because in
    Windows and NTFS filenames are stored case-preserving but must be
    searched case-insensitive in UTC2 encoding). Then the deletion change
    is logged to the NTFS metadata log and committed to disk, the change
    itself is made and committed to disk, then the completion record is
    placed in the log. And then it goes on to the next file.

    Taking that into account, 70 deletions on spinning rust in an overloaded directory is not a bad speed!

    In *nix systems, directories are essentially files in a particular
    format. Deletions are primarily changes to those files. You also need
    to remove the inode from the inode table (assuming - as is almost
    certainly the case here - that there are no other directory entries
    pointing to the same file), and again the inode tables are treated much
    like files. You still need to find the files in the directories, and
    sifting through 3.5 million files takes time on most filesystems (some
    have directory formats that are less bad for huge directories, but none
    are great for it). But the changes to the directories and the inode
    tables, with corresponding metadata log commits, is buffered up and effectively done in batches.

    Still, I'd not be holding my breath for clearing up such a mess - even
    on a Linux system with a PCIe SSD it would take uncomfortably long.



    --- Synchronet 3.19c-Linux NewsLink 1.113
  • From David Brown@david.brown@hesbynett.no to comp.lang.misc on Sun Dec 4 23:59:18 2022
    From Newsgroup: comp.lang.misc

    On 04/12/2022 19:21, James Harris wrote:
    On 04/12/2022 16:30, David Brown wrote:
    On 03/12/2022 20:34, James Harris wrote:

    ...

    Performance can be just as much a part of engineering a solution as
    correctness.

    ... (snipped comments which basically expanded on the above)


    It is easier to make correct code fast than to make fast code correct.
    It is easier to make clear code correct than to make correct code
    clear.   Write you code /clearly/ with an aim for /correctness/ -
    don't focus on performance.

    I understand where you are coming from but I have to say I think you are wrong in keeping downplaying performance. As I said, it /can be/ just as important as correctness in engineering a solution. And it might not be. There are different scenarios:

    If you are writing an application and you know that performance of it is
    not critical (and it contains no time-critical parts) then by all means focus laser-like on correctness.


    This means correctness first, performance a bonus.

    By contrast, if you are writing an application which has time-critical
    parts then you have to ensure time compliance. This case, like the first one, is simple.

    Here, you are making particular timing aspects a requirement - the
    program is only correctly implementing the required specification if it
    works within the time limits. Any other performance aspects (such as
    memory efficiency, or performance beyond the required time constraints)
    are secondary. Again - correctness first, performance a bonus.


    But there are areas of middle ground in which performance requirements
    are not so straightforward. Say you are writing an OS, specifying a language, or designing a library. You don't know in advance what will be
    the timing requirements of the apps that will use them. Nor do you know
    what crazy ways apps may use your OS, language or library. You have to anticipate users going off piste and stressing the design with larger elements and more of them than you would have thought possible.


    If you write an OS and you don't focus on correctness, it is /useless/.
    No one wants an OS where a file write will be fast but might make
    mistakes! You certainly put in lots of effort to making the OS
    efficient, but /never/ at the cost of making it do the /wrong/ thing.

    The case in point is two libraries: a memory allocator and a string
    library which uses the memory allocator. What I am saying to you is that such libraries need to be correct but they also need to scale to large strings, many strings, many string operations, many other non-string allocation activity, etc, because it's impossible to say just now how
    they will be used. It's no good to just "focus on correctness".


    I did not say that you should ignore performance or efficiency. I said /correctness/ trumps performance every time. /Every/ time. No exceptions.

    If particular specific performance expectations are part of the
    requirements, then they are part of /correctness/.

    If you are just looking for "as fast as practically possible given the
    budget constraints for the development process", then that's fine too -
    but it /always/ comes secondary to correctness.

    You can also sometimes define your requirements in a way that allows
    some kinds of "imperfections" - such as defining a lossy compression
    format for audio or video files. But that's just changing the
    specifications - "this function should be able to show 60 frames per
    second with no more than 10 dB increase in noise difference from the
    original and using no more than 10 MB memory". A /correct/ function implements those requirements even if it takes 100% cpu time.
    Additional effort into performance to reduce that cpu time, or to
    improve the quality to better than 10 dB, or to reduce the memory
    overhead, is useful. But you do not get to use 12 MB memory for a 10
    times speed increase - that function would be incorrect, and not satisfy
    the requirements. (You could go back to the customer and recommend a
    change to the requirements, of course.)


    --- Synchronet 3.19c-Linux NewsLink 1.113
  • From Bart@bc@freeuk.com to comp.lang.misc on Sun Dec 4 23:02:21 2022
    From Newsgroup: comp.lang.misc

    On 04/12/2022 15:56, James Harris wrote:
    On 04/12/2022 14:47, Bart wrote:
    On 04/12/2022 14:07, James Harris wrote:
    On 04/12/2022 13:27, David Brown wrote:

    ...

    You are asking what the allocator should do if the programmer using
    it writes nonsense?

    No, I was asking whether you intended that a programmer should be
    able to release memory in units other than those he used for the
    allocation requests. For example, consider an allocation from A to D
    when the programmer wants to release B to C within it. A < B < C < D:

       ----------------------------------------------------
       A        B   C                                      D

    I was asking whether you thought the programmer should be able to
    release B to C before releasing the other parts.

    I thought that was what you and Bart (who IIRC also mentioned
    something similar)

    I don't think so. You suggestion sounds a little crazy actually:
    someone allocates a buffer to store a string, which spans A to D
    exclusive, and which will have ensured that A was aligned, and D was
    aligned (for the next alloc)

    In the ABCD example I wasn't talking about strings.


    Later, they want to independently free a section of that from B to C
    exclusive, to turn that one block into three:

        A -> B  allocated
        B -> C  free
        C -> D  allocated

    So what happens to the alignment needs of B and C? What happens to the
    string that was stored in it, of which 2 remnants remain, and one
    chunk is missing?

    Again, I wasn't talking about strings but a general case. I thought it
    was you who, along with David, suggested a 'free' which included the
    length as in

      free(address, length)

    I think I know where you're coming from: because my allocator doesn't
    maintain any information about the allocation size, you assume its Free
    can be used on any block of arbitrary memory, include a chunk of a
    previously allocated block.

    Well, this depends. It wouldn't work on mine; there are two parts to it:

    (I) For blocks 2KB and below, a requested size N is rounded if needed to
    the next power of 2 (minimum 16, maximum 2048).

    The same rounding is performed when calling Free(P, N). So if N was 240:

    * N is rounded to 256 and 256 bytes are allocated (either from a
    freelist of 256-byte blocks, or from the pool)

    * If you later try and free the top 35 bytes of the 240, the 35 is
    rounded to 64, and that block is put into a free list of 64-byte blocks.
    When later allocated to another call, it will use 13 bytes
    ((240-35)-256) that belong to a different block as it extends past the original 256 bytes.

    * Further, the base address of 35-byte block will be 205 bytes into the original block, which is misaligned by 5 from a multiple of 8, and 13
    bytes from a multiple of 16 which is what my allocator assumes.

    * When you later free the remaining 205 bytes, that still rounds to 256,
    but now includes 51 bytes (256-51), that belong to that freed 64-byte
    chunk (35->64), which itself encroaches on the next.

    (II) Allocations over 2KB are also rounded to a power of 2 (up to 33MB
    where the rules change). So if N is 6000, this becomes 8192, which is requested directly from malloc.

    This now runs into trouble if you try and free the last 1000 of the
    6000, even though that 2KB block lies wholly within the 8192, because
    you are messing with the workings of malloc which /does/ record the sizes.

    Freeing a hole in the middle is not going to be much better in either
    case, because of this behind-scenes size rounding, assumptions about alignment, and the machinations of malloc.


    --- Synchronet 3.19c-Linux NewsLink 1.113
  • From David Brown@david.brown@hesbynett.no to comp.lang.misc on Mon Dec 5 00:12:30 2022
    From Newsgroup: comp.lang.misc

    On 04/12/2022 15:07, James Harris wrote:
    On 04/12/2022 13:27, David Brown wrote:
    On 03/12/2022 21:23, James Harris wrote:
    On 29/11/2022 16:54, David Brown wrote:

    I'm skipping the rest here - either you understand, appreciate and
    agree with what I wrote, or you don't.  (That means you either don't
    understand it, don't appreciate it, or just don't agree with it - not
    necessarily that I don't think you understand it.)  That's fair enough
    - it's your language and libraries, not mine.

    :-) I just wrote something similar to you in another reply.



    Don't design a language or library based on squeezing in as many
    features as you can imagine possibly being useful on a few occasions
    to a few people.  Design it simply and cleanly, in a way that can be >>>> expanded by users as needed, and where the implementation can be
    changed.

    I'd start with an object-based interface for an "Allocator", with
    methods :

         Allocator.alloc(size) -> pointer
         Allocator.alloc_zero(size) -> pointer
         Allocator.realloc(pointer, oldsize, newsize) -> pointer
         Allocator.free(pointer, size)

    Thanks for coming up with a specific suggestion. What would you want
    to happen if Allocator.free was asked to release a section of memory
    which was wholly within an allocated chunk?


    You are asking what the allocator should do if the programmer using it
    writes nonsense?

    No, I was asking whether you intended that a programmer should be able
    to release memory in units other than those he used for the allocation requests. For example, consider an allocation from A to D when the programmer wants to release B to C within it. A < B < C < D:

      ----------------------------------------------------
      A        B   C                                      D

    I was asking whether you thought the programmer should be able to
    release B to C before releasing the other parts.

    You mean the programmer does a single "malloc" for (D - A) bytes,
    getting pointer A as a result, and then later wants to call "free" to
    release bytes from B to C while keeping A to B and C to D? No, I don't
    think that should be considered for a general allocator. You only get
    to free the memory blocks you received, fully and completely. I suppose
    it is possible to think of a more specialised allocator that would allow freeing parts of a block like this, but it would be unusual.


    I thought that was what you and Bart (who IIRC also mentioned something similar) had in mind and I spend some time designing an allocator to
    support splitting memory in that way down to the byte level. It was an interesting exercise and I may use it one day but I thought you had some
    use cases in mind.

    Who cares?  Launch the nasal daemons.

    Never, never, and never!


    Garbage in, garbage out.

    You cannot give the program sensible answers to stupid questions. And
    no matter how much you try to check the program's requests for sanity
    and valid values, there will be a programmer who will outwit you and
    find new ways to be wrong that are beyond your imagination. If you have
    a low-level language that allows things like pointers and conversions
    between points and integers, this will happen /very/ easily, and there
    is nothing you can do to stop it.

    So as the language designer, and as the tools implementer, you get to
    say "These are the rules. Follow them, and your programs can do
    wonderful things. Break them, and I take no responsibility for the
    chaos that may follow". This is how it works for /every/ programming language. Some languages try to make ever more complicated schemes to
    detect rule-breaking and follow up with some kind of "controlled" error handling, but really that's just more rules. And the controlled error handling never really works in practice, because the programmer was not expecting an error - if he/she were, they would have fixed the bug in
    their code.

    (Of course you do your best to help programmers find these kinds of
    problems and fix them, at least in debug modes, and you don't go out of
    your way simply to make the results of undefined behaviour worse than necessary.)

    The concept of undefined behaviour - of "garbage in, garbage out" - is
    as old as computing:

    """
    On two occasions I have been asked, 'Pray, Mr. Babbage, if you put into
    the machine wrong figures, will the right answers come out?' I am not
    able rightly to apprehend the kind of confusion of ideas that could
    provoke such a question.

    Charles Babbage
    """


    --- Synchronet 3.19c-Linux NewsLink 1.113
  • From Bart@bc@freeuk.com to comp.lang.misc on Sun Dec 4 23:41:34 2022
    From Newsgroup: comp.lang.misc

    On 04/12/2022 22:23, David Brown wrote:
    On 04/12/2022 14:54, James Harris wrote:

    So even a single byte can make a difference. But because of alignment
    in what we have been talking about a programmer could gain 3, 7 or 15
    bytes in every allocation which he would not otherwise have had! These
    extra bytes would be completely free and would otherwise have been
    wasted. There is no good reason to prevent a client from using them.


    It is quite common to have string formats that support arbitrarily long strings with dynamically allocated memory (either as a contiguous
    buffer, or as a chain), and have a "short string optimisation" for
    holding data locally.  You do not allocate data for the short string,
    and you do not increase its buffer size, or faff around trying to get a
    byte or two extra.

    Why not? I've done that sometimes, for example:

    I have a counted-string data type, where string data is allocated from
    the heap.

    This string is not zero-terminated, so when I want to pass it to a API expecting a C-style zero-terminated string, it would mean creating a new
    heap copy with an extra byte for that zero.

    However, when an allocator rounds up, even if only to a multiple of 8,
    the chances are there will be a spare byte already there: I can just set
    to to zero (or the string library can do that when it created this string).

    You might say, why not just allocate an extra byte anyway, but:

    * Sometimes, that can mean allocating more memory (up to double with my allocators) just for that byte, because there is no reserve.

    * You don't want to start making assumptions that there will always be
    that terminator. It won't exist when making a slice or view into the
    middle of another string for example.

    It's better to keep this zero-terminator business strictly within FFI reqirements only and not build it in to your data type.

    --- Synchronet 3.19c-Linux NewsLink 1.113
  • From Dmitry A. Kazakov@mailbox@dmitry-kazakov.de to comp.lang.misc on Mon Dec 5 09:05:16 2022
    From Newsgroup: comp.lang.misc

    On 2022-12-04 23:44, David Brown wrote:

    The key to doing that kind of operation efficiently is to buffer them. Windows is too enthusiastic about committing everything to disk at every point.  So for each file, it has to find it in the directory (which is a big effort at the best of times - but made /vastly/ worse because in
    Windows and NTFS filenames are stored case-preserving but must be
    searched case-insensitive in UTC2 encoding).  Then the deletion change
    is logged to the NTFS metadata log and committed to disk, the change
    itself is made and committed to disk, then the completion record is
    placed in the log.  And then it goes on to the next file.

    Moreover, Windows does lots of garbage calls on the directory file,
    opening, closing, opening, closing (looking for desktop.ini in each
    directory! (:-)) etc. I once implemented a virtual file system for
    Windows and was amazed by the sheer amount of nonsense that is going on
    there.

    Still, I'd not be holding my breath for clearing up such a mess - even
    on a Linux system with a PCIe SSD it would take uncomfortably long.

    On bulk operations SSD's have performance problems of their own.
    --
    Regards,
    Dmitry A. Kazakov
    http://www.dmitry-kazakov.de

    --- Synchronet 3.19c-Linux NewsLink 1.113
  • From Dmitry A. Kazakov@mailbox@dmitry-kazakov.de to comp.lang.misc on Mon Dec 5 09:15:23 2022
    From Newsgroup: comp.lang.misc

    On 2022-12-05 00:12, David Brown wrote:

    The concept of undefined behaviour - of "garbage in, garbage out" - is
    as old as computing:

    """
    On two occasions I have been asked, 'Pray, Mr. Babbage, if you put into
    the machine wrong figures, will the right answers come out?' I am not
    able rightly to apprehend the kind of confusion of ideas that could
    provoke such a question.

    Charles Babbage
    """

    That is a cool quote!

    Another maxim:

    relax preconditions, strengthen postconditions
    --
    Regards,
    Dmitry A. Kazakov
    http://www.dmitry-kazakov.de

    --- Synchronet 3.19c-Linux NewsLink 1.113
  • From David Brown@david.brown@hesbynett.no to comp.lang.misc on Mon Dec 5 11:40:30 2022
    From Newsgroup: comp.lang.misc

    On 05/12/2022 09:15, Dmitry A. Kazakov wrote:
    On 2022-12-05 00:12, David Brown wrote:

    The concept of undefined behaviour - of "garbage in, garbage out" - is
    as old as computing:

    """
    On two occasions I have been asked, 'Pray, Mr. Babbage, if you put
    into the machine wrong figures, will the right answers come out?' I am
    not able rightly to apprehend the kind of confusion of ideas that
    could provoke such a question.

    Charles Babbage
    """

    That is a cool quote!


    Apparently it was members of parliament that asked him this!


    It never ceases to amaze me how many people don't understand it,
    however. People try to write libraries or functions that will somehow
    "work" even when given bad data, or cling to the believe that it is
    possible and desirable for a language to define the behaviour of
    everything. The result, inevitably, is extra inefficiencies for correct
    code, error paths that are completely untestable, and at best you simply
    push the bugs around to somewhere else in the code.

    When someone hits their thumb with a hammer, it is not the fault of the hammer, or the fault of the nail, and nor should the hammer designer be responsible for making sure the nail goes in even if you hit your thumb.
    If you are not confident in using the hammer safely and effectively,
    then it is /your/ responsibility, as the hammer user, to learn how to do
    it correctly, or to find alternative solutions to the problem if
    hammering is too risky.


    There's a thread on comp.lang.c at the moment from some twit who things allocation functions in C should have type "long" for their size
    argument rather than "size_t". His justification is that the allocation function can then check for negative values of the size and return an
    error. This is, of course, completely silly - it does not ensure the
    value is "safe" in any way, it wastes effort for correct code, and any programmer who writes code that tries to allocate negative space is
    highly unlikely to write suitable error handling.


    It's fine to have extra checks or controls for debugging and testing
    purposes. But you have to assume programmers will do their job.


    Another maxim:

       relax preconditions, strengthen postconditions


    That's not a great maxim IMHO. When you have good specifications for a function (or other piece of code), you /may/ relax the preconditions and
    you /may/ strengthen the postconditions, but that won't give you a
    better solution. Your task is to ensure that the code you write does
    not strengthen the preconditions and does not weaken the postconditions.

    (To be fair, code is rarely well enough specified!)

    --- Synchronet 3.19c-Linux NewsLink 1.113
  • From Dmitry A. Kazakov@mailbox@dmitry-kazakov.de to comp.lang.misc on Mon Dec 5 12:06:23 2022
    From Newsgroup: comp.lang.misc

    On 2022-12-05 11:40, David Brown wrote:

    Your task is to ensure that the code you write does
    not strengthen the preconditions and does not weaken the postconditions.

    That is the task of somebody who implements the specifications. An implementation that strengthens the precondition or weakens the
    postcondition is simply incorrect.

    The task of a specification designer is to mandate weakest precondition possible. Any precondition is a heavy burden on the code user and a
    source of bugs.

    (To be fair, code is rarely well enough specified!)

    People frequently confuse preconditions with erroneous inputs. The precondition or real sqrt is plain True (assuming types are enforced). Negative argument is as valid as positive:

    Pre: True
    Post: (x >= 0 and sqrt(x)**2 = x) or (x < 0 and exception raised)

    Now a bad design would be:

    Pre: x >= 0
    Post: sqrt(x)**2 = x

    Here you have undefined behavior for x < 0.
    --
    Regards,
    Dmitry A. Kazakov
    http://www.dmitry-kazakov.de

    --- Synchronet 3.19c-Linux NewsLink 1.113
  • From Bart@bc@freeuk.com to comp.lang.misc on Mon Dec 5 11:17:25 2022
    From Newsgroup: comp.lang.misc

    On 05/12/2022 11:06, Dmitry A. Kazakov wrote:
    On 2022-12-05 11:40, David Brown wrote:

    Your task is to ensure that the code you write does not strengthen the
    preconditions and does not weaken the postconditions.

    That is the task of somebody who implements the specifications. An implementation that strengthens the precondition or weakens the postcondition is simply incorrect.

    The task of a specification designer is to mandate weakest precondition possible. Any precondition is a heavy burden on the code user and a
    source of bugs.

    (To be fair, code is rarely well enough specified!)

    People frequently confuse preconditions with erroneous inputs. The precondition or real sqrt is plain True (assuming types are enforced). Negative argument is as valid as positive:

    Pre: True
    Post: (x >= 0 and sqrt(x)**2 = x) or (x < 0 and exception raised)

    Now a bad design would be:

    Pre: x >= 0
    Post: sqrt(x)**2 = x

    Here you have undefined behavior for x < 0.

    Not at all. For x<0, my sqrt() gives an invalid number (displayed as -1.#IND00, depending on the stringify library).

    Any attempt to use it for further calculation will propagate that error
    value. User programs can test for such values if they wish.


    --- Synchronet 3.19c-Linux NewsLink 1.113
  • From Dmitry A. Kazakov@mailbox@dmitry-kazakov.de to comp.lang.misc on Mon Dec 5 12:35:42 2022
    From Newsgroup: comp.lang.misc

    On 2022-12-05 12:17, Bart wrote:
    On 05/12/2022 11:06, Dmitry A. Kazakov wrote:
    On 2022-12-05 11:40, David Brown wrote:

    Your task is to ensure that the code you write does not strengthen
    the preconditions and does not weaken the postconditions.

    That is the task of somebody who implements the specifications. An
    implementation that strengthens the precondition or weakens the
    postcondition is simply incorrect.

    The task of a specification designer is to mandate weakest
    precondition possible. Any precondition is a heavy burden on the code
    user and a source of bugs.

    (To be fair, code is rarely well enough specified!)

    People frequently confuse preconditions with erroneous inputs. The
    precondition or real sqrt is plain True (assuming types are enforced).
    Negative argument is as valid as positive:

    Pre: True
    Post: (x >= 0 and sqrt(x)**2 = x) or (x < 0 and exception raised)

    Now a bad design would be:

    Pre: x >= 0
    Post: sqrt(x)**2 = x

    Here you have undefined behavior for x < 0.

    Not at all. For x<0, my sqrt() gives an invalid number (displayed as -1.#IND00, depending on the stringify library).

    You did not understand the point.

    Your sqrt is not real-value sqrt at all, because the result is not a
    real number. However, if we considered rather sqrt defined on some
    subset of R U NaN, here NaN is a not-a-number ideal. Then the point can
    be easily made. Good design:

    Pre: True
    Post: (x /= NaN and x >= 0 and sqrt(x)**2 = x) or
    (x /= NaN and x < 0 and sqrt(x) = NaN) or
    (x = NaN and sqrt(x) = NaN)

    Bad design:

    Pre: x /= NaN and x >= 0
    Post: sqrt(x)**2 = x

    (Of course IEEE 754 ideals was a very stupid idea from SW POV, but that
    is unrelated to the issue of undefined behavior.)
    --
    Regards,
    Dmitry A. Kazakov
    http://www.dmitry-kazakov.de

    --- Synchronet 3.19c-Linux NewsLink 1.113
  • From David Brown@david.brown@hesbynett.no to comp.lang.misc on Mon Dec 5 15:38:45 2022
    From Newsgroup: comp.lang.misc

    On 05/12/2022 12:06, Dmitry A. Kazakov wrote:
    On 2022-12-05 11:40, David Brown wrote:

    Your task is to ensure that the code you write does not strengthen the
    preconditions and does not weaken the postconditions.

    That is the task of somebody who implements the specifications. An implementation that strengthens the precondition or weakens the postcondition is simply incorrect.


    Yes.

    The task of a specification designer is to mandate weakest precondition possible. Any precondition is a heavy burden on the code user and a
    source of bugs.


    No.

    A good strong precondition makes the use of the function clear, the implementation simpler and more efficient, and the whole thing easier to
    test. See below in your example of square root.


    A weak precondition makes the function more flexible to call, but harder
    to implement. The opposite is true for a strong precondition. A
    designer striving to make the precondition as weak as possible, or as
    strong as possible, is failing in his/her job - the precondition should
    be an appropriate choice for the task in hand.

    (The weakest precondition is "nothing", the strongest postcondition is "everything". This is known in computer science as the function "magic"
    - it takes any inputs whatsoever, and always gives the user exactly what
    they wanted. It may be ideal from a client's viewpoint, but it cannot
    be implemented - and is thus useless as a specification.)


    (To be fair, code is rarely well enough specified!)

    People frequently confuse preconditions with erroneous inputs.

    If the inputs satisfy the preconditions, they are not erroneous.

    (What satisfies the preconditions may vary at run-time - for example, a
    "draw rectangle" function may have a precondition that the coordinates
    must be within the current window frame. That kind of thing is usually
    a bad idea for specifications, but people don't always write good specifications!)

    The
    precondition or real sqrt is plain True (assuming types are enforced). Negative argument is as valid as positive:

    Pre: True
    Post: (x >= 0 and sqrt(x)**2 = x) or (x < 0 and exception raised)

    Now a bad design would be:

    Pre: x >= 0
    Post: sqrt(x)**2 = x

    Here you have undefined behavior for x < 0.



    The second version is /far/ better than the first.

    The first version has a confusing specification. It leaves the user
    wondering what it really does when reading the precondition - will this function give me the square root of negative numbers? Is it giving
    complex results, or saturating, or expecting me to multiply the result
    by "i" myself?

    Then the user reads the postcondition and sees that for negative x it
    raises an exception. Now my code needs to handle the possibility of an exception - that's not what I want! I know I never give the sqrt
    function a negative number, so the exception handling path will never be
    taken and cannot be tested without writing extra test code to simulate
    an impossible situation. That's more extra code, more testing, more
    risk of errors, more inefficiencies, for exactly /zero/ benefit.

    Or I could leave the exception unhandled, relying on code higher up the
    food chain to do it - possibly written by a different programmer who
    knows nothing about square roots or their errors. That means code that
    calls the code that calls sqrt could get unexpected exceptions, usually
    not clearly specified or declared in the function type - basically, it's
    a hidden "goto" to an almost random part of the program with unexpected, untested and unplanned consequences.

    Exceptions are a valid mechanism for handling unusual and rare run-time circumstances, such as a database transaction failure, loss of contact
    in a network connection, and so on. They are /not/ a crutch for poor programming and coding errors!

    And of course the implementer of the first version can't make a nice
    efficient solution that boils down to a single "fsqrtd" assembly
    instruction (or whatever your cpu calls it), but has add extra checks
    that will never trigger in normal usage, and some kind of exception
    throwing mechanism.



    The second version of the specification is clear and simple. The user
    knows they need to call the function with non-negative values, and they
    are quite happy with that - they wouldn't be doing anything else anyway.
    The implementer knows the function is only ever called with
    non-negative values and can efficiently calculate the square root. The responsibility for using appropriate inputs is where it should be - with
    the caller. The responsibility for handling exceptions is no longer an
    issue for anyone.


    The first version requires so much extra work for both the user and the implementer that it is higher risk - despite apparently being "safer"
    because it has no undefined behaviour in the sqrt function itself. The
    second version has no undefined behaviour in practice because the user
    does not call the function with inappropriate inputs. The first version /does/ have undefined behaviour in practice, because exceptions like
    these are often not handled appropriately or fully tested.


    --- Synchronet 3.19c-Linux NewsLink 1.113
  • From Dmitry A. Kazakov@mailbox@dmitry-kazakov.de to comp.lang.misc on Mon Dec 5 16:14:33 2022
    From Newsgroup: comp.lang.misc

    On 2022-12-05 15:38, David Brown wrote:
    On 05/12/2022 12:06, Dmitry A. Kazakov wrote:
    On 2022-12-05 11:40, David Brown wrote:

    The task of a specification designer is to mandate weakest
    precondition possible. Any precondition is a heavy burden on the code
    user and a source of bugs.


    No.

    A good strong precondition makes the use of the function clear, the implementation simpler and more efficient, and the whole thing easier to test.

    Strong precondition is a booby trap.

    A weak precondition makes the function more flexible to call, but harder
    to implement.

    Weak precondition guaranties defined behavior.

    People frequently confuse preconditions with erroneous inputs.

    If the inputs satisfy the preconditions, they are not erroneous.

    They are. Violated precondition is a bug. Erroneous input is just
    anticipated event, e.g. a user typed 68 minutes.

    (What satisfies the preconditions may vary at run-time - for example, a "draw rectangle" function may have a precondition that the coordinates
    must be within the current window frame.  That kind of thing is usually
    a bad idea for specifications, but people don't always write good specifications!)

    The precondition or real sqrt is plain True (assuming types are
    enforced). Negative argument is as valid as positive:

    Pre: True
    Post: (x >= 0 and sqrt(x)**2 = x) or (x < 0 and exception raised)

    Now a bad design would be:

    Pre: x >= 0
    Post: sqrt(x)**2 = x

    Here you have undefined behavior for x < 0.

    The second version is /far/ better than the first.

    The first version has a confusing specification.  It leaves the user wondering what it really does when reading the precondition - will this function give me the square root of negative numbers?  Is it giving
    complex results, or saturating, or expecting me to multiply the result
    by "i" myself?

    It unambiguously states that sqrt has a real argument.

    Then the user reads the postcondition and sees that for negative x it
    raises an exception.  Now my code needs to handle the possibility of an exception - that's not what I want!

    If you do not want it, don't pass negative number as an argument.

    I know I never give the sqrt
    function a negative number,

    And how do know that?

    so the exception handling path will never be
    taken and cannot be tested without writing extra test code to simulate
    an impossible situation.  That's more extra code, more testing, more
    risk of errors, more inefficiencies, for exactly /zero/ benefit.

    Pre- and postconditions are for correctness proofs, not for testing. It
    is stupid to test program proven correct.

    Or I could leave the exception unhandled,

    You cannot do that. Again, it is about proofs. If your subprogram has no exception in its postcondition you could not prove it correct unless x
    is non-negative.

    If you do not prove correctness, then exception propagation is just
    defined behavior as opposed to undefined one.

    Exceptions are a valid mechanism for handling unusual and rare run-time circumstances, such as a database transaction failure, loss of contact
    in a network connection, and so on.  They are /not/ a crutch for poor programming and coding errors!

    Sure undefined behavior is much better than that! (:-))

    The second version of the specification is clear and simple.

    It leaves behavior undefined without any good reason. Strong
    precondition if not an utter necessity is a grave design fault.

     The implementer knows the function is only ever called with
    non-negative values and can efficiently calculate the square root.

    He does not know that without correctness proofs, which are rarely
    possible and rarely used.

    The
    responsibility for using appropriate inputs is where it should be - with
    the caller.

    Exactly, shoveling your problems to the caller, causing distributed
    overhead and making sure of having untraceable bugs in the production
    code under to motto:

    "no qualified programmer would ever do X"


    The responsibility for handling exceptions is no longer an
    issue for anyone.

    Replaced by obligatory reading core dumps...
    --
    Regards,
    Dmitry A. Kazakov
    http://www.dmitry-kazakov.de

    --- Synchronet 3.19c-Linux NewsLink 1.113
  • From David Brown@david.brown@hesbynett.no to comp.lang.misc on Mon Dec 5 17:56:06 2022
    From Newsgroup: comp.lang.misc

    On 05/12/2022 16:14, Dmitry A. Kazakov wrote:
    On 2022-12-05 15:38, David Brown wrote:
    On 05/12/2022 12:06, Dmitry A. Kazakov wrote:
    On 2022-12-05 11:40, David Brown wrote:

    The task of a specification designer is to mandate weakest
    precondition possible. Any precondition is a heavy burden on the code
    user and a source of bugs.


    No.

    A good strong precondition makes the use of the function clear, the
    implementation simpler and more efficient, and the whole thing easier
    to test.

    Strong precondition is a booby trap.

    Only if it is not clear and documented.


    A weak precondition makes the function more flexible to call, but
    harder to implement.

    Weak precondition guaranties defined behavior.

    Which is pointless if the defined behaviour is useless or wrong.

    Remember the old joke about politicians in a crisis? "We must do
    something! This is something - therefore we must do it!".

    Put your effort into defining useful handling of real requirements - not desperate attempts to assign meanings to meaningless situations.


    People frequently confuse preconditions with erroneous inputs.

    If the inputs satisfy the preconditions, they are not erroneous.

    They are. Violated precondition is a bug. Erroneous input is just anticipated event, e.g. a user typed 68 minutes.


    That all depends on your definitions of "error" and "bug". Input from
    outside - such as user input - must be sanitized and checked, with
    appropriate feedback to the user or other data source when there is
    invalid data. If bad data is passed around inside the code, that is a bug.

    If you specify your square root function with a precondition of "true",
    then no input is erroneous. Some values might be senseless in
    mathematical terms, and be the result of bugs in the code, but you have specified your function to be happy with senseless input - it is
    therefore not an error.

    (What satisfies the preconditions may vary at run-time - for example,
    a "draw rectangle" function may have a precondition that the
    coordinates must be within the current window frame.  That kind of
    thing is usually a bad idea for specifications, but people don't
    always write good specifications!)

    The precondition or real sqrt is plain True (assuming types are
    enforced). Negative argument is as valid as positive:

    Pre: True
    Post: (x >= 0 and sqrt(x)**2 = x) or (x < 0 and exception raised)

    Now a bad design would be:

    Pre: x >= 0
    Post: sqrt(x)**2 = x

    Here you have undefined behavior for x < 0.

    The second version is /far/ better than the first.

    The first version has a confusing specification.  It leaves the user
    wondering what it really does when reading the precondition - will
    this function give me the square root of negative numbers?  Is it
    giving complex results, or saturating, or expecting me to multiply the
    result by "i" myself?

    It unambiguously states that sqrt has a real argument.


    But not that the result of the function is real - that is not specified.

    Then the user reads the postcondition and sees that for negative x it
    raises an exception.  Now my code needs to handle the possibility of
    an exception - that's not what I want!

    If you do not want it, don't pass negative number as an argument.


    I don't want the possibility to exist in my code. I don't want to use a function that might throw an exception - that would just mean more
    effort on my part to deal with the possibility and test that handler code.

    I know I never give the sqrt function a negative number,

    And how do know that?


    That's my side of the bargain.

    Software development is all about contracts. A function has a
    precondition and a postcondition - it promises that if the precondition
    is fulfilled before the call, the postcondition will be fulfilled after
    the call. It says /nothing/ about what will happen if the precondition
    is not fulfilled. As a caller of the function, my job is to make sure
    the precondition is fulfilled, and then I will rely on the postcondition
    after the call. Just as the caller can rely on the output of sqrt(9) to
    be 3, since that's the promise made by the function specification, the function can rely on x >= 0, since that's the promise made by the caller.

    If you can't rely on the calling code to do its job, what makes you
    think you can rely on the called function to do its job? What makes you
    think you can rely on anything here? The whole exercise becomes
    pointless. The whole concept of a "precondition" is nonsensical if the function implementation cannot rely on it being true!


    so the exception handling path will never be taken and cannot be
    tested without writing extra test code to simulate an impossible
    situation.  That's more extra code, more testing, more risk of errors,
    more inefficiencies, for exactly /zero/ benefit.

    Pre- and postconditions are for correctness proofs, not for testing. It
    is stupid to test program proven correct.


    Well, that's /very/ questionable - you still have to test that the
    proofs have been made correctly. Correctness proofs and tests work
    together, not as alternatives.

    And preconditions and postconditions are primarily contracts for the implementation of functions and calling code, with correctness proofs an optional (but useful) extra.


    However, I was not talking about testing the pre- or postconditions - I
    was talking about testing the exception handling code I, as the caller,
    have to write since the function may throw an exception.

    Or I could leave the exception unhandled,

    You cannot do that. Again, it is about proofs. If your subprogram has no exception in its postcondition you could not prove it correct unless x
    is non-negative.

    If you do not prove correctness, then exception propagation is just
    defined behavior as opposed to undefined one.


    This kind of misuse of exceptions to hide bugs in code is not about
    proofs - it is about arse-covering. It has nothing to do with making
    code safer or more reliable, or being able to quantify risks or
    reliability. It's all about being able to stand up in court and say
    that plane crash was not /my/ fault - I put checks in the code and threw
    an exception. It doesn't matter if the exception was part of the cause
    of the problem by forcing the function caller to add so much extra code
    that it increased the risks. You don't care that instead of a clearly
    bad value being shown on a display, the attempted negative square root
    now lead to an unhandled exception that brought down the whole system.
    It is not /your/ fault.

    Oh wait, you claimed that the person writing the calling code couldn't possibly have forgotten to handle the exception, because then they
    wouldn't have been able to prove their code is correct. So you trust
    them to have the skills, training, and resources to do formal
    correctness proofs on their code - but you don't trust them to be able
    to see if a value is negative? Do you not see anything wrong with that "logic" ?

    Exceptions are a valid mechanism for handling unusual and rare
    run-time circumstances, such as a database transaction failure, loss
    of contact in a network connection, and so on.  They are /not/ a
    crutch for poor programming and coding errors!

    Sure undefined behavior is much better than that! (:-))


    The point about undefined behaviour is that it does not happen. You
    define what /does/ happen. You say the function is defined for x >= 0.
    The caller ensures that it is called with x >= 0. The undefined
    behaviour does not happen - end of story.

    It is /better/ to leave situations that must not be allowed to occur, as undefined. It is clearer and simpler for everyone. One set of rules - everyone has a job to do, and knows what it is.

    With your specification, roles are mixed - should the caller check for negative x? Is that the function's job? If the caller has checked
    this, does it really have to handle an exception? Logically no, but
    maybe the language requires it somewhere. It's all a bloody mess, and
    much more likely to end in tears.

    The second version of the specification is clear and simple.

    It leaves behavior undefined without any good reason. Strong
    precondition if not an utter necessity is a grave design fault.


    /Please/ read a book about how to write specifications for functions,
    and how to program using them. There's a good one called "Programming
    from Specifications" that is freely available - though it is quite
    theoretical and mathematical.

    In short - you define behaviour for what you want to happen for valid
    inputs. It doesn't matter what happens for invalid inputs - there is no sensible value (in the real numbers) to assign to the square root of
    negative numbers. Any attempt to specify that in a square root function
    is bound to fail - so don't try. Complicating things by saying your
    function called "sqrt" isn't really a square root function, but a sort
    of mishmash that sometimes does square roots, and sometimes jumps wildly
    about in your code is not helpful.

    You are concerned about proving code correctness. With my
    specifications, I know that after "y = sqrt(x)", y * y equals x. That's
    nice, and very helpful in a correctness proof - as well as simply making
    the code clear to the reader. With your specification, you don't know
    that. It might be true - or it might not. Perhaps "y" doesn't exist
    because it has been skipped by the hidden long jump. Again, a mess.

    How about debugging? After all, real programmers make mistakes. With undefined behaviour, debugging tools (such as "sanitizer" options in
    modern compilers) can spot many kinds of undefined behaviour and give
    the developer a nice report about the problem. It can be combined with run-time precondition checkers for even more helpful information.

    But your version doesn't have undefined behaviour - throwing an
    exception is part of the specification. That means the debugger or
    sanitizer can't help you here - the caller gave a negative number, but
    the precondition checked out okay. The problem propagates until you
    have lost all chance of finding where it originated.

      The implementer knows the function is only ever called with
    non-negative values and can efficiently calculate the square root.

    He does not know that without correctness proofs, which are rarely
    possible and rarely used.


    He knows because he wrote correct code that calls the function
    appropriately.

    The responsibility for using appropriate inputs is where it should be
    - with the caller.

    Exactly, shoveling your problems to the caller, causing distributed
    overhead and making sure of having untraceable bugs in the production
    code under to motto:

       "no qualified programmer would ever do X"


    No, it is not passing on /your/ problems to anyone. It is letting the
    caller take responsibility for /their/ code. That is their job - not
    mine, as the function implementer. My job is to make sure the
    postcondition is fulfilled, assuming the precondition is satisfied. The caller programmer's job is to make sure the precondition is fulfilled,
    and then they can rely on the postcondition.

    Jumbling this up and making a precondition of "true" helps no one - it
    just messes up the roles so that no one knows what is going on or who is
    doing what.


    The responsibility for handling exceptions is no longer an issue for
    anyone.

    Replaced by obligatory reading core dumps...


    --- Synchronet 3.19c-Linux NewsLink 1.113
  • From Dmitry A. Kazakov@mailbox@dmitry-kazakov.de to comp.lang.misc on Mon Dec 5 21:39:10 2022
    From Newsgroup: comp.lang.misc

    On 2022-12-05 17:56, David Brown wrote:
    On 05/12/2022 16:14, Dmitry A. Kazakov wrote:
    On 2022-12-05 15:38, David Brown wrote:
    On 05/12/2022 12:06, Dmitry A. Kazakov wrote:
    On 2022-12-05 11:40, David Brown wrote:

    The task of a specification designer is to mandate weakest
    precondition possible. Any precondition is a heavy burden on the
    code user and a source of bugs.


    No.

    A good strong precondition makes the use of the function clear, the
    implementation simpler and more efficient, and the whole thing easier
    to test.

    Strong precondition is a booby trap.

    Only if it is not clear and documented.

    I see it in the boldest font available: "garbage ahead, proceed at your
    own risk."

    A weak precondition makes the function more flexible to call, but
    harder to implement.

    Weak precondition guaranties defined behavior.

    Which is pointless if the defined behaviour is useless or wrong.

    Any defined behavior is better than any undefined one.

    People frequently confuse preconditions with erroneous inputs.

    If the inputs satisfy the preconditions, they are not erroneous.

    They are. Violated precondition is a bug. Erroneous input is just
    anticipated event, e.g. a user typed 68 minutes.

    That all depends on your definitions of "error" and "bug".

    Error is an undesired, exceptional state. Bug is an illegal state.

    If you specify your square root function with a precondition of "true",
    then no input is erroneous.

    No. It is so that input is a caller's bug = contract violation. Negative
    input is an erroneous, exceptional input. No difference to inputs
    causing + to overflow. Care to write a strong precondition for +, *, /,
    gamma etc?

    Some values might be senseless in
    mathematical terms, and be the result of bugs in the code, but you have specified your function to be happy with senseless input - it is
    therefore not an error.

    Yes, it is not a bug, because a computational model is only a model.
    Most modeled things are incomputable. Being inadequate model is not yet
    a bug so long you can detect and handle inadequacy.

    (What satisfies the preconditions may vary at run-time - for example,
    a "draw rectangle" function may have a precondition that the
    coordinates must be within the current window frame.  That kind of
    thing is usually a bad idea for specifications, but people don't
    always write good specifications!)

    The precondition or real sqrt is plain True (assuming types are
    enforced). Negative argument is as valid as positive:

    Pre: True
    Post: (x >= 0 and sqrt(x)**2 = x) or (x < 0 and exception raised)

    Now a bad design would be:

    Pre: x >= 0
    Post: sqrt(x)**2 = x

    Here you have undefined behavior for x < 0.

    The second version is /far/ better than the first.

    The first version has a confusing specification.  It leaves the user
    wondering what it really does when reading the precondition - will
    this function give me the square root of negative numbers?  Is it
    giving complex results, or saturating, or expecting me to multiply
    the result by "i" myself?

    It unambiguously states that sqrt has a real argument.

    But not that the result of the function is real - that is not specified.

    Because it cannot specify what is mathematically incorrect.

    Then the user reads the postcondition and sees that for negative x it
    raises an exception.  Now my code needs to handle the possibility of
    an exception - that's not what I want!

    If you do not want it, don't pass negative number as an argument.

    I don't want the possibility to exist in my code.

    Use a different type. E.g. define sqrt on a subtype:

    subtype Non_Negative_Float is Float range 0.0..Float'Last;
    function Sqrt (X : Non_Negative_Float) return Float;

    Note that this does not save you. Because it simply moves the
    postcondition to the constraint check.

    sqrt(-1.0)

    will still propagate exception: Constraint_Error instead of, maybe, Numeric_Error.

    I don't want to use a
    function that might throw an exception - that would just mean more
    effort on my part to deal with the possibility and test that handler code.

    You don't use arithmetic +,-,*,/? That is very courageous...

    I know I never give the sqrt function a negative number,

    And how do know that?

    That's my side of the bargain.

    Not if your code is used by somebody else in turn.

    Software development is all about contracts.  A function has a
    precondition and a postcondition - it promises that if the precondition
    is fulfilled before the call, the postcondition will be fulfilled after
    the call.  It says /nothing/ about what will happen if the precondition
    is not fulfilled.

    Yep, and we don't want such contracts unless absolutely necessary.

    If you can't rely on the calling code to do its job, what makes you
    think you can rely on the called function to do its job?

    Each party must fulfill its side of contract.

    The advise about strengths addresses the way contracts are designed to
    be useful, safer and easier to fulfill.

    The whole concept of a "precondition" is nonsensical if the
    function implementation cannot rely on it being true!

    Which is why the precondition must be weakest possible. Thanks.

    so the exception handling path will never be taken and cannot be
    tested without writing extra test code to simulate an impossible
    situation.  That's more extra code, more testing, more risk of
    errors, more inefficiencies, for exactly /zero/ benefit.

    Pre- and postconditions are for correctness proofs, not for testing.
    It is stupid to test program proven correct.

    Well, that's /very/ questionable - you still have to test that the
    proofs have been made correctly.

    That is a test of the prover, not a test of the application program. It
    is as stupid as write compiler, linker, OS tests for the application.
    That prover, compiler, linker, CPU, Maxwell's equations work is a *precondition*!

    Correctness proofs and tests work
    together, not as alternatives.

    Nope. Applied to the *same* object, they are complementary.

    However, I was not talking about testing the pre- or postconditions - I
    was talking about testing the exception handling code I, as the caller,
    have to write since the function may throw an exception.

    No, you have to write a test since the caller is not proven not to cause
    the function to propagate an exception. Again, what is the object here?
    What do you want to test? The caller, the callee, the compiler, the CPU?

    Or I could leave the exception unhandled,

    You cannot do that. Again, it is about proofs. If your subprogram has
    no exception in its postcondition you could not prove it correct
    unless x is non-negative.

    If you do not prove correctness, then exception propagation is just
    defined behavior as opposed to undefined one.

    This kind of misuse of exceptions to hide bugs in code is not about
    proofs - it is about arse-covering.

    No, it is about keeping contracts clean. It is impossible to define
    model types that would produce mathematically correct results. Start
    with + and produce types of X and Y such that the result is always mathematically correct X + Y. Exceptions is a method to fill the gaps
    between the model and the problem space without breaking the abstraction altogether.

    Oh wait, you claimed that the person writing the calling code couldn't possibly have forgotten to handle the exception, because then they
    wouldn't have been able to prove their code is correct.  So you trust
    them to have the skills, training, and resources to do formal
    correctness proofs on their code - but you don't trust them to be able
    to see if a value is negative?

    Yep, and this is trivially a halting problem:

    X := 2.0;
    if HALT(p) then
    X := -1.0;
    end if;
    X := sqrt (X);

    And, BTW, prover does proofs, not the developer.

    In the above the prover would say: "Sorry, I cannot prove the
    postcondition saying no exception on negative argument. Try better."

    There are basically two cases #1 with prover, #2 without.

    #1 Precondition plays no role. It is strictly same to prove that X>=0
    and that sqrt does raise exception. One directly implies another.

    #2 Exception was massive advantage. Since it cannot be proven that X>=0,
    then it is expected to be X<0 and exception propagation allows most
    eager detection. While precondition hides the problem. sqrt(-1.0) might
    return 13.2 and the application will continue normally generating garbage.

    Exceptions are a valid mechanism for handling unusual and rare
    run-time circumstances, such as a database transaction failure, loss
    of contact in a network connection, and so on.  They are /not/ a
    crutch for poor programming and coding errors!

    Sure undefined behavior is much better than that! (:-))


    The point about undefined behaviour is that it does not happen.

    sqrt(-1.0)

    It is /better/ to leave situations that must not be allowed to occur, as undefined.

    sqrt(-1.0)

    With your specification, roles are mixed - should the caller check for negative x?

    Nothing in the contract of sqrt requires it. You are confusing contract parties. Without seeing the caller's contract nothing can be said.

    Is that the function's job?

    The function's job is to implement the contract. How it does this is not
    the caller's business.

    If the caller has checked
    this, does it really have to handle an exception?

    It is not sqrt business what the caller does with the result. It is
    between the caller and the caller's caller.

    The second version of the specification is clear and simple.

    It leaves behavior undefined without any good reason. Strong
    precondition if not an utter necessity is a grave design fault.

    /Please/ read a book about how to write specifications for functions,
    and how to program using them.  There's a good one called "Programming
    from Specifications" that is freely available - though it is quite theoretical and mathematical.

    In short - you define behaviour for what you want to happen for valid inputs.

    -1.0 is a valid input, it is a value of the type. -1.0 is a real value.

    [...]

    How about debugging?  After all, real programmers make mistakes.  With undefined behaviour, debugging tools (such as "sanitizer" options in
    modern compilers) can spot many kinds of undefined behaviour and give
    the developer a nice report about the problem.

    You fundamentally cannot detect undefined behavior, because any behavior
    can realize when undefined. It is allowed to fire a gamma jet that would
    roast the silly programmer...

    But your version doesn't have undefined behaviour - throwing an
    exception is part of the specification.

    Yep, and it can be trivially verified by tests or in any debugger.

      The implementer knows the function is only ever called with
    non-negative values and can efficiently calculate the square root.

    He does not know that without correctness proofs, which are rarely
    possible and rarely used.

    He knows because he wrote correct code that calls the function appropriately.

    He thinks he knows...

    The responsibility for using appropriate inputs is where it should be
    - with the caller.

    Exactly, shoveling your problems to the caller, causing distributed
    overhead and making sure of having untraceable bugs in the production
    code under to motto:

        "no qualified programmer would ever do X"

    No, it is not passing on /your/ problems to anyone.  It is letting the caller take responsibility for /their/ code.

    "no qualified programmer would forget to check his code"
    --
    Regards,
    Dmitry A. Kazakov
    http://www.dmitry-kazakov.de

    --- Synchronet 3.19c-Linux NewsLink 1.113
  • From James Harris@james.harris.1@gmail.com to comp.lang.misc on Mon Dec 5 22:08:50 2022
    From Newsgroup: comp.lang.misc

    On 04/12/2022 22:23, David Brown wrote:
    On 04/12/2022 14:54, James Harris wrote:
    On 04/12/2022 12:31, David Brown wrote:
    On 03/12/2022 20:17, James Harris wrote:

    ...

    For example, who decided that buffers in a chain couldn't be
    enlarged? Who invented that prohibition and why is it there?

    ...

    Look, at the lowest level, buffers are /never/ enlarged.

    Rubbish! Just because /you/ never want such a thing does not mean that
    others do not want it, nor does your dislike for it or your lack of
    experience of it mean that it's a bad idea.


    They are /not/ enlarged.

    On the contrary, in what I proposed buffers /would be/ enlarged. You are evidently arguing against my proposal when you don't understand it.
    Please put aside whatever model you have in mind and consider the
    proposal if you want to raise objections to it!! We've been arguing
    about this for over a week, now, and it's not a good use of time for objections to be unrelated to the proposal they are supposed to be
    against. :-(


    You allocate some memory for a buffer.  You allocate more memory later
    for something else - it gets put after the first buffer.  How are you
    going to enlarge the first buffer?  Stomp all over the second allocation?

    I cannot work out what model you have in mind but it doesn't seem to
    match what I proposed. To illustrate, say there is a chain of two buffers:

    buffer A is 1000 bytes
    buffer B is 100 bytes

    A string already occupies all of A and B. The application tells the
    string library to append 80 bytes to the string. There is no room in B
    so the string library asks the memory allocator to resize B in place to
    make it large enough. Let's say the allocator finds 20 bytes of free
    space after B but not the 80 asked for. It makes B 20 bytes longer. As
    it's not long enough the string library then requests space for another buffer, C, and add it to the chain; it would then put 20 bytes in B and
    60 in C.

    Please note especially that last sentence.

    What allows the string library to know? Two calls:

    resize(B, ....) - requests resizing of B in place
    msize(B) - requests the actual size of B

    You may not like the proposal but if you you are going to continue to
    object to it please say /why/ it wouldn't work rather than just saying
    "that's not how it's done". How things are done ATM in your experience
    is irrelevant.

    ...

    If the string type is part of the language's basic standard library (and
    I'd recommend that),

    OK.

    then the buffer size is going to be picked to match
    the allocation size from the standard library allocator used.  It would
    be downright idiotic to have an allocator that, say, rounds sizes up to 32-byte blocks and then have a string structure that was 20 bytes long, supporting short strings of up to 18 bytes.  It would be even dafter to have a string structure that is of varying size and have users
    re-allocate it to grow it to use the extra size that the library (not
    the user) knows is always there.

    There is no need to make the string library dependent on a particular allocator.

    ...

    Well, the software would be in layers.

          app
           |
           V
      string library
           |
           V
       allocator


    Yes.

    What I've proposed in this thread is a simple and small change to the
    allocator which would allow the string library to make best use of the
    spaces which the allocator returns to the string library.


    No.

    What do you mean by No? Are you trying to tell me I didn't propose what
    I proposed?


    The simple method is that the string library knows the size of chunks provided by the allocator, and always uses that information.

    That is a poor design. The dependency you propose is unnecessary. There
    is no need for a library to be bound to a particular allocator.


    I am not sure why you keep objecting to that but I am pretty sure you
    and I aren't going to agree on this stuff. Still, no problem.
    Agreement is not mandatory. :-)


    It would be boring indeed if we all agreed!  /Disagreement/ is mandatory for an interesting thread :-)

    But we do agree on many things.

    Indeed, as programmers I get the impression that we agree on most
    things. These discussions, by their nature, focus on the areas of disagreement.
    --
    James Harris


    --- Synchronet 3.19c-Linux NewsLink 1.113
  • From James Harris@james.harris.1@gmail.com to comp.lang.misc on Mon Dec 5 22:25:15 2022
    From Newsgroup: comp.lang.misc

    On 04/12/2022 22:59, David Brown wrote:
    On 04/12/2022 19:21, James Harris wrote:

    ...

    The case in point is two libraries: a memory allocator and a string
    library which uses the memory allocator. What I am saying to you is
    that such libraries need to be correct but they also need to scale to
    large strings, many strings, many string operations, many other
    non-string allocation activity, etc, because it's impossible to say
    just now how they will be used. It's no good to just "focus on
    correctness".


    I did not say that you should ignore performance or efficiency.  I said /correctness/ trumps performance every time.  /Every/ time.  No exceptions.

    You can say it yet again but that doesn't mean it's true. Correctness is essential, of course, but for some programs which need hard real-time guarantees performance is equally as important.


    If particular specific performance expectations are part of the requirements, then they are part of /correctness/.

    Indeed.


    If you are just looking for "as fast as practically possible given the budget constraints for the development process", then that's fine too -
    but it /always/ comes secondary to correctness.

    That is at least a development on what you said before but it is still impractical. If you implemented a simple n-squared sort "due to budget constraints" it may work fine for weeks but then hang a system which got
    hit with a much larger data set. It does not scale. I dread to think how
    much 'professional' and paid-for software has followed similar
    principles but I've been unfortunate enough to use some of it.
    --
    James Harris


    --- Synchronet 3.19c-Linux NewsLink 1.113
  • From David Brown@david.brown@hesbynett.no to comp.lang.misc on Mon Dec 5 23:29:50 2022
    From Newsgroup: comp.lang.misc

    On 05/12/2022 21:39, Dmitry A. Kazakov wrote:
    On 2022-12-05 17:56, David Brown wrote:
    On 05/12/2022 16:14, Dmitry A. Kazakov wrote:
    On 2022-12-05 15:38, David Brown wrote:
    On 05/12/2022 12:06, Dmitry A. Kazakov wrote:
    On 2022-12-05 11:40, David Brown wrote:

    The task of a specification designer is to mandate weakest
    precondition possible. Any precondition is a heavy burden on the
    code user and a source of bugs.


    No.

    A good strong precondition makes the use of the function clear, the
    implementation simpler and more efficient, and the whole thing
    easier to test.

    Strong precondition is a booby trap.

    Only if it is not clear and documented.

    I see it in the boldest font available: "garbage ahead, proceed at your
    own risk."

    A weak precondition makes the function more flexible to call, but
    harder to implement.

    Weak precondition guaranties defined behavior.

    Which is pointless if the defined behaviour is useless or wrong.

    Any defined behavior is better than any undefined one.


    Wrong. I've given clear arguments why that is the case - if they don't convince you, there's little point in repeating them.

    People frequently confuse preconditions with erroneous inputs.

    If the inputs satisfy the preconditions, they are not erroneous.

    They are. Violated precondition is a bug. Erroneous input is just
    anticipated event, e.g. a user typed 68 minutes.

    That all depends on your definitions of "error" and "bug".

    Error is an undesired, exceptional state. Bug is an illegal state.


    That's one choice of definitions - it is very far from a universal
    choice, but we'll go with them for now.

    If you specify your square root function with a precondition of
    "true", then no input is erroneous.

    No. It is so that input is a caller's bug = contract violation. Negative input is an erroneous, exceptional input. No difference to inputs
    causing + to overflow. Care to write a strong precondition for +, *, /, gamma etc?

    int add_int(int a, int b);
    precondition: (a + b >= INT_MIN) && (a + b <= INT_MAX)

    It's not rocket science.


    Some values might be senseless in mathematical terms, and be the
    result of bugs in the code, but you have specified your function to be
    happy with senseless input - it is therefore not an error.

    Yes, it is not a bug, because a computational model is only a model.
    Most modeled things are incomputable. Being inadequate model is not yet
    a bug so long you can detect and handle inadequacy.

    (What satisfies the preconditions may vary at run-time - for
    example, a "draw rectangle" function may have a precondition that
    the coordinates must be within the current window frame.  That kind
    of thing is usually a bad idea for specifications, but people don't
    always write good specifications!)

    The precondition or real sqrt is plain True (assuming types are
    enforced). Negative argument is as valid as positive:

    Pre: True
    Post: (x >= 0 and sqrt(x)**2 = x) or (x < 0 and exception raised)

    Now a bad design would be:

    Pre: x >= 0
    Post: sqrt(x)**2 = x

    Here you have undefined behavior for x < 0.

    The second version is /far/ better than the first.

    The first version has a confusing specification.  It leaves the user >>>> wondering what it really does when reading the precondition - will
    this function give me the square root of negative numbers?  Is it
    giving complex results, or saturating, or expecting me to multiply
    the result by "i" myself?

    It unambiguously states that sqrt has a real argument.

    But not that the result of the function is real - that is not specified.

    Because it cannot specify what is mathematically incorrect.


    And yet you want to specify what a square root function should do when
    given this mathematically unworkable input.

    Then the user reads the postcondition and sees that for negative x
    it raises an exception.  Now my code needs to handle the possibility >>>> of an exception - that's not what I want!

    If you do not want it, don't pass negative number as an argument.

    I don't want the possibility to exist in my code.

    Use a different type. E.g. define sqrt on a subtype:

       subtype Non_Negative_Float is Float range 0.0..Float'Last;
       function Sqrt (X : Non_Negative_Float) return Float;


    You could do that, yes.

    Note that this does not save you. Because it simply moves the
    postcondition to the constraint check.

       sqrt(-1.0)

    will still propagate exception: Constraint_Error instead of, maybe, Numeric_Error.

    That's all up to the typing system you use. Presumably that would be a compile-time error, not an exception.


    I don't want to use a function that might throw an exception - that
    would just mean more effort on my part to deal with the possibility
    and test that handler code.

    You don't use arithmetic +,-,*,/? That is very courageous...


    These don't throw exceptions in the languages I use. In some languages,
    they are specified with constraints on their inputs. In other
    languages, arbitrary precision arithmetic is used to get correct answers.

    (In high level programming, such as Python, I do make use of exceptions
    for the appropriate purposes - exceptional run-time events, rather than program errors.)


    I know I never give the sqrt function a negative number,

    And how do know that?

    That's my side of the bargain.

    Not if your code is used by somebody else in turn.

    They do their job too.

    I don't comprehend how you assume the person calling "sqrt" is so
    incompetent, and have such an incompetent development process and team,
    that they might call it with a negative number and not find the problem
    unless the implementation throws an exception. And yet you think that
    same programmer is competent to write essentially untestable exception handling code without flaw, dealing with the vastly more complex
    situation of code that runs partially and stops unexpectedly.


    Software development is all about contracts.  A function has a
    precondition and a postcondition - it promises that if the
    precondition is fulfilled before the call, the postcondition will be
    fulfilled after the call.  It says /nothing/ about what will happen if
    the precondition is not fulfilled.

    Yep, and we don't want such contracts unless absolutely necessary.


    They are necessary. We want them. They make life simple and clear, and everyone knows what they are doing and gets it right.

    If you can't rely on the calling code to do its job, what makes you
    think you can rely on the called function to do its job?

    Each party must fulfill its side of contract.


    Yes.

    The advise about strengths addresses the way contracts are designed to
    be useful, safer and easier to fulfill.


    But it is wrong advice - it makes everything /harder/.

    The whole concept of a "precondition" is nonsensical if the function
    implementation cannot rely on it being true!

    Which is why the precondition must be weakest possible. Thanks.


    From the function implementers viewpoint, it should be as /strong/ as possible. And regardless of how weak or strong it is, the function implementer can rely upon it - assuming you are actually doing software engineering and not just faffing around getting paid by the number of
    lines of code you throw out.

    so the exception handling path will never be taken and cannot be
    tested without writing extra test code to simulate an impossible
    situation.  That's more extra code, more testing, more risk of
    errors, more inefficiencies, for exactly /zero/ benefit.

    Pre- and postconditions are for correctness proofs, not for testing.
    It is stupid to test program proven correct.

    Well, that's /very/ questionable - you still have to test that the
    proofs have been made correctly.

    That is a test of the prover, not a test of the application program. It
    is as stupid as write compiler, linker, OS tests for the application.
    That prover, compiler, linker, CPU, Maxwell's equations work is a *precondition*!

    Correctness proofs and tests work together, not as alternatives.

    Nope. Applied to the *same* object, they are complementary.


    No, they work together. Some things are best verified by proofs, some
    things by testing, some things by both. A /vastly/ better programmer
    than you or I said "Be careful of this code. I have only proven it
    correct, not tested it."

    However, I was not talking about testing the pre- or postconditions -
    I was talking about testing the exception handling code I, as the
    caller, have to write since the function may throw an exception.

    No, you have to write a test since the caller is not proven not to cause
    the function to propagate an exception. Again, what is the object here?
    What do you want to test? The caller, the callee, the compiler, the CPU?

    Or I could leave the exception unhandled,

    You cannot do that. Again, it is about proofs. If your subprogram has
    no exception in its postcondition you could not prove it correct
    unless x is non-negative.

    If you do not prove correctness, then exception propagation is just
    defined behavior as opposed to undefined one.

    This kind of misuse of exceptions to hide bugs in code is not about
    proofs - it is about arse-covering.

    No, it is about keeping contracts clean.

    What could be cleaner than the contract for a square root function that returns the square root of valid inputs? Certainly not some drivel that
    pulls exception handling and all its types, hierarchies, handlers and mechanisms into the picture.

    It is impossible to define
    model types that would produce mathematically correct results. Start
    with + and produce types of X and Y such that the result is always mathematically correct X + Y. Exceptions is a method to fill the gaps between the model and the problem space without breaking the abstraction altogether.

    Oh wait, you claimed that the person writing the calling code couldn't
    possibly have forgotten to handle the exception, because then they
    wouldn't have been able to prove their code is correct.  So you trust
    them to have the skills, training, and resources to do formal
    correctness proofs on their code - but you don't trust them to be able
    to see if a value is negative?

    Yep, and this is trivially a halting problem:

       X := 2.0;
       if HALT(p) then
          X := -1.0;
       end if;
       X := sqrt (X);

    And, BTW, prover does proofs, not the developer.


    So you are not sure if the programmer might try to write a halting
    machine inside their function that calls square root? Okay, I suppose,
    if you are /that/ paranoid - but why should the person specifying or implementing the square root function have to deal with that?

    In the above the prover would say: "Sorry, I cannot prove the
    postcondition saying no exception on negative argument. Try better."

    There are basically two cases #1 with prover, #2 without.

    #1 Precondition plays no role. It is strictly same to prove that X>=0
    and that sqrt does raise exception. One directly implies another.

    #2 Exception was massive advantage. Since it cannot be proven that X>=0, then it is expected to be X<0 and exception propagation allows most
    eager detection. While precondition hides the problem. sqrt(-1.0) might return 13.2 and the application will continue normally generating garbage.

    Exceptions are a valid mechanism for handling unusual and rare
    run-time circumstances, such as a database transaction failure, loss
    of contact in a network connection, and so on.  They are /not/ a
    crutch for poor programming and coding errors!

    Sure undefined behavior is much better than that! (:-))


    The point about undefined behaviour is that it does not happen.

    sqrt(-1.0)

    It is /better/ to leave situations that must not be allowed to occur,
    as undefined.

    sqrt(-1.0)

    The problem lies with the programmer that uses the function, or their development team or process that failed to catch the bug. The function specifier is not responsible for that, and cannot fix the problem
    anyway. So why make everything more complicated and higher risk?

    Do you really think it is realistic that in a real program, a mistake
    leading to an attempt at finding a negative square root is because
    someone wrote "sqrt(-1.0)", or perhaps "sqrt(-x)" instead of "sqrt(x)" ?
    No, the realistic source of the problem would be far back in the code
    - throwing an exception at this point will help very little. Do you
    really think the code would be able to recover sensibly and handle the situation gracefully when you throw an exception? No, that has no
    rooting in real life - at the point where the negative square root is
    called, the code's calculations and data are so screwed up that you
    won't be able to help.


    With your specification, roles are mixed - should the caller check for
    negative x?

    Nothing in the contract of sqrt requires it. You are confusing contract parties. Without seeing the caller's contract nothing can be said.


    The specifications for the function are the contract.

    Is that the function's job?

    The function's job is to implement the contract. How it does this is not
    the caller's business.


    Equally, the caller's job is to ensure that it fulfils /its/ side of the contract (the precondition). How it does this is not the callee's business.

    If the caller has checked this, does it really have to handle an
    exception?

    It is not sqrt business what the caller does with the result. It is
    between the caller and the caller's caller.


    Indeed that is true. And equally, it is not sqrt's business how the
    caller ensures that it has satisfied the precondition before calling it.
    The specified of sqrt has no interest in making a pointlessly weak precondition that forces a pointlessly complex postcondition. Just let
    the caller ensure that it only passes non-negative numbers.

    The second version of the specification is clear and simple.

    It leaves behavior undefined without any good reason. Strong
    precondition if not an utter necessity is a grave design fault.

    /Please/ read a book about how to write specifications for functions,
    and how to program using them.  There's a good one called "Programming
    from Specifications" that is freely available - though it is quite
    theoretical and mathematical.

    In short - you define behaviour for what you want to happen for valid
    inputs.

    -1.0 is a valid input, it is a value of the type. -1.0 is a real value.


    Not to a square root function that is worthy of the name.

    [...]

    How about debugging?  After all, real programmers make mistakes.  With
    undefined behaviour, debugging tools (such as "sanitizer" options in
    modern compilers) can spot many kinds of undefined behaviour and give
    the developer a nice report about the problem.

    You fundamentally cannot detect undefined behavior, because any behavior
    can realize when undefined. It is allowed to fire a gamma jet that would roast the silly programmer...


    Yes. But you can spot many types of undefined behaviour, both through
    static analysis and run-time checking in debug modes.

    But your version doesn't have undefined behaviour - throwing an
    exception is part of the specification.

    Yep, and it can be trivially verified by tests or in any debugger.


    It is not a bug. The debugger and tools don't know this defined
    behaviour is not intentional - because it is fully defined behaviour
    that the function caller can rely on.

      The implementer knows the function is only ever called with
    non-negative values and can efficiently calculate the square root.

    He does not know that without correctness proofs, which are rarely
    possible and rarely used.

    He knows because he wrote correct code that calls the function
    appropriately.

    He thinks he knows...

    The responsibility for using appropriate inputs is where it should
    be - with the caller.

    Exactly, shoveling your problems to the caller, causing distributed
    overhead and making sure of having untraceable bugs in the production
    code under to motto:

        "no qualified programmer would ever do X"

    No, it is not passing on /your/ problems to anyone.  It is letting the
    caller take responsibility for /their/ code.

    "no qualified programmer would forget to check his code"


    Programmers can make mistakes - including the people calling sqrt, the
    people implementing it, and the people specifying it. It makes sense to
    make life as clear and simple as practically possible for them.

    When you give someone a recipe for merengues, you say "Beat the egg
    whites then slowly add the sugar while beating all the time in order to
    make the merengue mixture". You don't add "alternatively, add the sugar
    then try to beat it in order to make a mess". In pretty much every walk
    of life, we specify the expected input to get the desired output, and do
    not concern ourselves about what happens when someone puts in garbage to
    the set of instructions. Why would you want to do something different
    for programming?



    --- Synchronet 3.19c-Linux NewsLink 1.113
  • From James Harris@james.harris.1@gmail.com to comp.lang.misc on Mon Dec 5 22:32:48 2022
    From Newsgroup: comp.lang.misc

    On 04/12/2022 23:02, Bart wrote:
    On 04/12/2022 15:56, James Harris wrote:

    ...

    Again, I wasn't talking about strings but a general case. I thought it
    was you who, along with David, suggested a 'free' which included the
    length as in

       free(address, length)

    ...

    Freeing a hole in the middle is not going to be much better in either
    case, because of this behind-scenes size rounding, assumptions about alignment, and the machinations of malloc.

    Sure. I don't have a use case, either. I was just interested in the fact
    that unmap appears to allow frees from within the middle of allocations
    and your proposal allowed similar.

    You mention malloc and alignment but if arbitrary frees were to be
    allowed then they'd probably have to be at the granularity of individual memory cells; the structures which managed the allocations would have to
    be stored elsewhere, not as malloc-style headers between each block.
    --
    James Harris



    --- Synchronet 3.19c-Linux NewsLink 1.113
  • From James Harris@james.harris.1@gmail.com to comp.lang.misc on Mon Dec 5 22:38:21 2022
    From Newsgroup: comp.lang.misc

    On 04/12/2022 23:12, David Brown wrote:
    On 04/12/2022 15:07, James Harris wrote:
    On 04/12/2022 13:27, David Brown wrote:

    ...

    I thought that was what you and Bart (who IIRC also mentioned
    something similar) had in mind and I spend some time designing an
    allocator to support splitting memory in that way down to the byte
    level. It was an interesting exercise and I may use it one day but I
    thought you had some use cases in mind.

    Who cares?  Launch the nasal daemons.

    Never, never, and never!


    Garbage in, garbage out.

    You cannot give the program sensible answers to stupid questions.  And
    no matter how much you try to check the program's requests for sanity
    and valid values, there will be a programmer who will outwit you and
    find new ways to be wrong that are beyond your imagination.  If you have
    a low-level language that allows things like pointers and conversions between points and integers, this will happen /very/ easily, and there
    is nothing you can do to stop it.

    ...

    The concept of undefined behaviour - of "garbage in, garbage out" - is
    as old as computing:

    Garbage out is not the same as undefined behaviour. Here's an example of garbage in leading to garbage out

    Enter your age: 1000
    You owe us $-1,234,567

    Here's an example of UB

    Enter your age: 1000
    Deleting files in your home directory


    Nasal demons should never be permitted.
    --
    James Harris


    --- Synchronet 3.19c-Linux NewsLink 1.113
  • From James Harris@james.harris.1@gmail.com to comp.lang.misc on Mon Dec 5 23:15:40 2022
    From Newsgroup: comp.lang.misc

    On 05/12/2022 11:35, Dmitry A. Kazakov wrote:
    On 2022-12-05 12:17, Bart wrote:
    On 05/12/2022 11:06, Dmitry A. Kazakov wrote:

    ...

    People frequently confuse preconditions with erroneous inputs. The
    precondition or real sqrt is plain True (assuming types are
    enforced). Negative argument is as valid as positive:

    Pre: True
    Post: (x >= 0 and sqrt(x)**2 = x) or (x < 0 and exception raised)

    Now a bad design would be:

    Pre: x >= 0
    Post: sqrt(x)**2 = x

    Here you have undefined behavior for x < 0.

    The behaviour of a function not meeting a precondition may not be
    evident in the code but may well be defined by the rules of the language.


    Not at all. For x<0, my sqrt() gives an invalid number (displayed as
    -1.#IND00, depending on the stringify library).

    You did not understand the point.

    Your sqrt is not real-value sqrt at all, because the result is not a
    real number. However, if we considered rather sqrt defined on some
    subset of R U NaN, here NaN is a not-a-number ideal. Then the point can
    be easily made. Good design:

    Pre: True
    Post: (x /= NaN and x >= 0 and sqrt(x)**2 = x) or
          (x /= NaN and x <  0 and sqrt(x) = NaN)  or
          (x  = NaN and sqrt(x) = NaN)

    Beware comparing floats for equality. sqrt(x)**2 may not exactly equal x
    - which rather upsets the whole idea of such precise and tautologous postconditions, doesn't it!
    --
    James Harris

    --- Synchronet 3.19c-Linux NewsLink 1.113
  • From James Harris@james.harris.1@gmail.com to comp.lang.misc on Mon Dec 5 23:36:40 2022
    From Newsgroup: comp.lang.misc

    On 05/12/2022 14:38, David Brown wrote:

    ...

    Exceptions are a valid mechanism for handling unusual and rare run-time circumstances, such as a database transaction failure, loss of contact
    in a network connection, and so on.  They are /not/ a crutch for poor programming and coding errors!

    That's one school of thought. Another is that exceptions be used to
    indicate also errors which a programmer would not normally want to test
    for each time they can occur so that his code can focus on the logic of
    the task at hand, e.g. OOM in

    p = memory_allocate(SIZE);

    or IO error from

    nbytes = read(fd, &buf, BUFSIZE);
    --
    James Harris


    --- Synchronet 3.19c-Linux NewsLink 1.113
  • From James Harris@james.harris.1@gmail.com to comp.lang.misc on Mon Dec 5 23:40:50 2022
    From Newsgroup: comp.lang.misc

    On 05/12/2022 16:56, David Brown wrote:

    ...

    Remember the old joke about politicians in a crisis?  "We must do something!  This is something - therefore we must do it!".

    That's known as the Politician's Syllogism: "We must do something. This
    is something. Therefore, we must do this."

    https://en.wikipedia.org/wiki/Politician%27s_syllogism
    --
    James Harris


    --- Synchronet 3.19c-Linux NewsLink 1.113
  • From James Harris@james.harris.1@gmail.com to comp.lang.misc on Mon Dec 5 23:47:53 2022
    From Newsgroup: comp.lang.misc

    On 05/12/2022 20:39, Dmitry A. Kazakov wrote:
    On 2022-12-05 17:56, David Brown wrote:

    ...

    Any defined behavior is better than any undefined one.

    Indeed!

    ...

    If you specify your square root function with a precondition of
    "true", then no input is erroneous.

    No. It is so that input is a caller's bug = contract violation. Negative input is an erroneous, exceptional input. No difference to inputs
    causing + to overflow. Care to write a strong precondition for +, *, /, gamma etc?

    Rather than a contract (which can lead to an error if the precondition
    is not met) it may be better to /define/ the range of possible inputs to
    a function: a call which has parameters which do not meet the
    definitions will not even match with the function which cannot handle
    them. For example,

    sqrt(-4.0)

    would not match with a definition

    function sqrt(real value :: value >= 0) -> real

    but would match with

    function sqrt(real value) -> complex
    --
    James Harris


    --- Synchronet 3.19c-Linux NewsLink 1.113
  • From David Brown@david.brown@hesbynett.no to comp.lang.misc on Tue Dec 6 08:46:11 2022
    From Newsgroup: comp.lang.misc

    On 05/12/2022 23:08, James Harris wrote:
    On 04/12/2022 22:23, David Brown wrote:
    On 04/12/2022 14:54, James Harris wrote:
    On 04/12/2022 12:31, David Brown wrote:
    On 03/12/2022 20:17, James Harris wrote:

    ...

    For example, who decided that buffers in a chain couldn't be
    enlarged? Who invented that prohibition and why is it there?

    ...

    Look, at the lowest level, buffers are /never/ enlarged.

    Rubbish! Just because /you/ never want such a thing does not mean
    that others do not want it, nor does your dislike for it or your lack
    of experience of it mean that it's a bad idea.


    They are /not/ enlarged.

    On the contrary, in what I proposed buffers /would be/ enlarged. You are evidently arguing against my proposal when you don't understand it.
    Please put aside whatever model you have in mind and consider the
    proposal if you want to raise objections to it!! We've been arguing
    about this for over a week, now, and it's not a good use of time for objections to be unrelated to the proposal they are supposed to be
    against. :-(


    You allocate some memory for a buffer.  You allocate more memory later
    for something else - it gets put after the first buffer.  How are you
    going to enlarge the first buffer?  Stomp all over the second allocation?

    I cannot work out what model you have in mind but it doesn't seem to
    match what I proposed. To illustrate, say there is a chain of two buffers:

      buffer A is 1000 bytes
      buffer B is 100 bytes

    A string already occupies all of A and B. The application tells the
    string library to append 80 bytes to the string. There is no room in B
    so the string library asks the memory allocator to resize B in place to
    make it large enough. Let's say the allocator finds 20 bytes of free
    space after B but not the 80 asked for. It makes B 20 bytes longer. As
    it's not long enough the string library then requests space for another buffer, C, and add it to the chain; it would then put 20 bytes in B and
    60 in C.


    The first extension of B is an example of pissing around wasting effort
    to squeeze in a few extra bytes. The rest is done by a /new/
    allocation, not an enlargement. That means all the time spend trying to figure out if you have an extra byte or two is wasted.

    Please note especially that last sentence.

    What allows the string library to know? Two calls:

      resize(B, ....) - requests resizing of B in place
      msize(B) - requests the actual size of B

    You may not like the proposal but if you you are going to continue to
    object to it please say /why/ it wouldn't work rather than just saying "that's not how it's done". How things are done ATM in your experience
    is irrelevant.


    Oh, I am sure it could be made to work.

    The big question is why anyone would bother with the large amount of
    extra effort and complication of code and data structures, and very significant extra run-time overhead on the off-chance that they might occasionally save a small number of bytes of heap. The answer, of
    course, is that the would not do so.

    There are basically two situations for strings. One case is that you
    know its size - it's a literal, or immutable object. Allocate one
    single buffer big enough to hold it, and you're done. No enlargement.

    The other case is that it is unknown in size. The simple handling is to
    use "realloc" as needed, growing as necessary. That's going to be very efficient for a lot of cases, but poor for big strings. Or allocate
    buffers appropriately as it grows, keeping a chain.

    Why should you not faff around with "resize", "get_real_size" and all
    your other unnecessary extras? Because it is is /slow/. It is extra
    effort and library calls that slow down everything for so little
    potential benefit.

    Memory allocation is slow. Freeing memory is slow. You do it as rarely
    as you can, and you do it as locally as you can. When you need
    something that might grow or shrink by small sizes, you track it locally
    in the data structure - your string, or dynamic array, or whatever, has
    a "size" and a "capacity" concept. If "size" is already at "capacity"
    and you need to grow, you take a /big/ lump of memory from the next
    level, so that you won't have to ask for more any time soon. At the
    library level you have an allocator that handles mid-sized lumps, from
    pools that are kept per thread. When these run low, it asks for more
    from the OS. And so on.

    Any time the size of something is dynamic, you ask for memory in
    generous lumps - usually in a power-of-two size for best alignment and
    easy fitting from the allocator, or at least with a power-of-two factor
    (such as a multiple of 32).

    There are always trade-offs between wasting some memory, and avoiding
    extra calls to allocators or having extra code to track the last few
    bytes. For real code, you can afford to waste a /lot/ of memory and get
    more efficient results.

    ...

    If the string type is part of the language's basic standard library
    (and I'd recommend that),

    OK.

    then the buffer size is going to be picked to match the allocation
    size from the standard library allocator used.  It would be downright
    idiotic to have an allocator that, say, rounds sizes up to 32-byte
    blocks and then have a string structure that was 20 bytes long,
    supporting short strings of up to 18 bytes.  It would be even dafter
    to have a string structure that is of varying size and have users
    re-allocate it to grow it to use the extra size that the library (not
    the user) knows is always there.

    There is no need to make the string library dependent on a particular allocator.


    No, but there are benefits in optimising it based on knowledge about the allocator.

    If your string library already knows the size of allocation lumps, there
    is never any point in calling "resize" - it will never give you
    anything. You want to make the basic "string" structure a neat size and
    with enough space for short strings (perhaps 12 - 20 characters), but
    you make the lumps it allocate sized to be multiples of your basic
    allocation size.



    --- Synchronet 3.19c-Linux NewsLink 1.113
  • From David Brown@david.brown@hesbynett.no to comp.lang.misc on Tue Dec 6 08:56:02 2022
    From Newsgroup: comp.lang.misc

    On 05/12/2022 23:25, James Harris wrote:
    On 04/12/2022 22:59, David Brown wrote:
    On 04/12/2022 19:21, James Harris wrote:

    ...

    The case in point is two libraries: a memory allocator and a string
    library which uses the memory allocator. What I am saying to you is
    that such libraries need to be correct but they also need to scale to
    large strings, many strings, many string operations, many other
    non-string allocation activity, etc, because it's impossible to say
    just now how they will be used. It's no good to just "focus on
    correctness".


    I did not say that you should ignore performance or efficiency.  I
    said /correctness/ trumps performance every time.  /Every/ time.  No
    exceptions.

    You can say it yet again but that doesn't mean it's true. Correctness is essential, of course, but for some programs which need hard real-time guarantees performance is equally as important.

    What part of "specific timing requirements are part of correctness" is
    giving you trouble here? Other requirements can be part of correctness
    too - if the customer says the program must be written in Quick Basic,
    then a much faster and neater alternative in Turbo Pascal is /wrong/.

    It really is impossible to stress this too much. Correctness is always
    key - it doesn't matter how efficient or performant an incorrect program
    is. If you do not understand that, you are in the wrong profession.



    If particular specific performance expectations are part of the
    requirements, then they are part of /correctness/.

    Indeed.


    If you are just looking for "as fast as practically possible given the
    budget constraints for the development process", then that's fine too
    - but it /always/ comes secondary to correctness.

    That is at least a development on what you said before but it is still impractical. If you implemented a simple n-squared sort "due to budget constraints" it may work fine for weeks but then hang a system which got
    hit with a much larger data set. It does not scale. I dread to think how much 'professional' and paid-for software has followed similar
    principles but I've been unfortunate enough to use some of it.


    The problem is not from programmers focusing on correctness. The main
    problem in software development is failures at the specification level.
    Your example is a good case in point - the "optimal" sorting method is highly dependent on what you are sorting, how big the data set is, how
    it is organised, what systems will be used for sorting it, and so on. A
    heap sort or quick sort is no more correct than a bubble sort if a
    bucket sort is needed. If the person specifying the task has not given
    the right requirements, then the result will never be good.

    So in many practical cases, the coder needs to help the specifier
    determine what they really want. (In too many practical cases, the
    coder /is/ the specifier.)

    --- Synchronet 3.19c-Linux NewsLink 1.113
  • From Dmitry A. Kazakov@mailbox@dmitry-kazakov.de to comp.lang.misc on Tue Dec 6 09:31:13 2022
    From Newsgroup: comp.lang.misc

    On 2022-12-06 00:15, James Harris wrote:
    On 05/12/2022 11:35, Dmitry A. Kazakov wrote:
    On 2022-12-05 12:17, Bart wrote:
    On 05/12/2022 11:06, Dmitry A. Kazakov wrote:

    ...

    People frequently confuse preconditions with erroneous inputs. The
    precondition or real sqrt is plain True (assuming types are
    enforced). Negative argument is as valid as positive:

    Pre: True
    Post: (x >= 0 and sqrt(x)**2 = x) or (x < 0 and exception raised)

    Now a bad design would be:

    Pre: x >= 0
    Post: sqrt(x)**2 = x

    Here you have undefined behavior for x < 0.

    The behaviour of a function not meeting a precondition may not be
    evident in the code but may well be defined by the rules of the language.

    No, that would evidently mean that the precondition is still True:

    (x >= 0) or not (x >= 0) = True

    So the result is

    Pre: True
    Post: (x >= 0 and sqrt(x)**2 = x) or
    (not (x >= 0) and language defined behavior)

    Not at all. For x<0, my sqrt() gives an invalid number (displayed as
    -1.#IND00, depending on the stringify library).

    You did not understand the point.

    Your sqrt is not real-value sqrt at all, because the result is not a
    real number. However, if we considered rather sqrt defined on some
    subset of R U NaN, here NaN is a not-a-number ideal. Then the point
    can be easily made. Good design:

    Pre: True
    Post: (x /= NaN and x >= 0 and sqrt(x)**2 = x) or
           (x /= NaN and x <  0 and sqrt(x) = NaN)  or
           (x  = NaN and sqrt(x) = NaN)

    Beware comparing floats for equality. sqrt(x)**2 may not exactly equal x
    - which rather upsets the whole idea of such precise and tautologous postconditions, doesn't it!

    It is a simplified version. Numerical approximations are usually
    specified to be exact to the last bit of the mantissa, so in the end a
    large postcondition formula would indeed have = somewhere in it.
    --
    Regards,
    Dmitry A. Kazakov
    http://www.dmitry-kazakov.de

    --- Synchronet 3.19c-Linux NewsLink 1.113
  • From Dmitry A. Kazakov@mailbox@dmitry-kazakov.de to comp.lang.misc on Tue Dec 6 09:43:54 2022
    From Newsgroup: comp.lang.misc

    On 2022-12-06 00:47, James Harris wrote:

    Rather than a contract (which can lead to an error if the precondition
    is not met) it may be better to /define/ the range of possible inputs to
    a function: a call which has parameters which do not meet the
    definitions will not even match with the function which cannot handle
    them. For example,

      sqrt(-4.0)

    would not match with a definition

      function sqrt(real value :: value >= 0) -> real

    but would match with

      function sqrt(real value) -> complex

    It is untyped. The formal proof is trivial, I gave it already:

    X := 4.0;
    if HALT(p) then
    X := -4.0;
    end if;
    sqrt (X); -- What is the type of?

    The type is undecidable in a legal program, ergo, types are broken.

    The general point is. You cannot map *all* correctness onto types. Types
    are too weak for that. You cannot express *all* behavior in terms of
    types. The example with sqrt perfectly illustrates that.
    --
    Regards,
    Dmitry A. Kazakov
    http://www.dmitry-kazakov.de

    --- Synchronet 3.19c-Linux NewsLink 1.113
  • From Dmitry A. Kazakov@mailbox@dmitry-kazakov.de to comp.lang.misc on Tue Dec 6 12:22:50 2022
    From Newsgroup: comp.lang.misc

    On 2022-12-05 23:29, David Brown wrote:
    On 05/12/2022 21:39, Dmitry A. Kazakov wrote:
    On 2022-12-05 17:56, David Brown wrote:
    On 05/12/2022 16:14, Dmitry A. Kazakov wrote:
    On 2022-12-05 15:38, David Brown wrote:
    On 05/12/2022 12:06, Dmitry A. Kazakov wrote:
    On 2022-12-05 11:40, David Brown wrote:

    If you specify your square root function with a precondition of
    "true", then no input is erroneous.

    No. It is so that input is a caller's bug = contract violation.
    Negative input is an erroneous, exceptional input. No difference to
    inputs causing + to overflow. Care to write a strong precondition for
    +, *, /, gamma etc?

    int add_int(int a, int b);
    precondition: (a + b >= INT_MIN) && (a + b <= INT_MAX)

    It's not rocket science.

    It is! Since the precondition here is not written in the object
    language. Remember, you argued that it is all for the caller's benefit.
    How the caller is supposed to check it? To learn, so to say, the full
    scale of benefits bestowed on it? (:-))

    Anyway, I presume it is finest design when X+Y returns garbage on
    overflow, underflow etc?

    (What satisfies the preconditions may vary at run-time - for
    example, a "draw rectangle" function may have a precondition that
    the coordinates must be within the current window frame.  That kind >>>>> of thing is usually a bad idea for specifications, but people don't >>>>> always write good specifications!)

    The precondition or real sqrt is plain True (assuming types are
    enforced). Negative argument is as valid as positive:

    Pre: True
    Post: (x >= 0 and sqrt(x)**2 = x) or (x < 0 and exception raised)

    Now a bad design would be:

    Pre: x >= 0
    Post: sqrt(x)**2 = x

    Here you have undefined behavior for x < 0.

    The second version is /far/ better than the first.

    The first version has a confusing specification.  It leaves the
    user wondering what it really does when reading the precondition -
    will this function give me the square root of negative numbers?  Is >>>>> it giving complex results, or saturating, or expecting me to
    multiply the result by "i" myself?

    It unambiguously states that sqrt has a real argument.

    But not that the result of the function is real - that is not specified.

    Because it cannot specify what is mathematically incorrect.

    And yet you want to specify what a square root function should do when
    given this mathematically unworkable input.

    Absolutely. Square root must indicate this unfortunate state. Again what
    else it should do? Format the hard drive?

    Then the user reads the postcondition and sees that for negative x
    it raises an exception.  Now my code needs to handle the
    possibility of an exception - that's not what I want!

    If you do not want it, don't pass negative number as an argument.

    I don't want the possibility to exist in my code.

    Use a different type. E.g. define sqrt on a subtype:

        subtype Non_Negative_Float is Float range 0.0..Float'Last;
        function Sqrt (X : Non_Negative_Float) return Float;

    You could do that, yes.

    Note that this does not save you. Because it simply moves the
    postcondition to the constraint check.

        sqrt(-1.0)

    will still propagate exception: Constraint_Error instead of, maybe,
    Numeric_Error.

    That's all up to the typing system you use.  Presumably that would be a compile-time error, not an exception.

    Nope, that is again a halting problem.

    I don't want to use a function that might throw an exception - that
    would just mean more effort on my part to deal with the possibility
    and test that handler code.

    You don't use arithmetic +,-,*,/? That is very courageous...

    These don't throw exceptions in the languages I use.

    These have *defined* behavior in any decent language. E.g. 1.0/0.0 is
    defined as NaN, which is hugely inferior to raising exception, but still
    a defined behavior.

    In some languages,
    they are specified with constraints on their inputs.  In other
    languages, arbitrary precision arithmetic is used to get correct answers.

    Really? Try

    (9999999.0 ** (9999999.0 ** (99999999.0 ** 99999999.99)))

    Again, integer, real, complex numbers are incomputable. Any
    implementation of is broken.

    BTW, if fixed precision floats are broken in a countable finite subset,
    the arbitrary precision ones are broken in an infinite subset, e.g.
    basically everywhere. (:-))

    I know I never give the sqrt function a negative number,

    And how do know that?

    That's my side of the bargain.

    Not if your code is used by somebody else in turn.

    They do their job too.

    I don't comprehend how you assume the person calling "sqrt" is so incompetent, and have such an incompetent development process and team,
    that they might call it with a negative number and not find the problem unless the implementation throws an exception.

    It has nothing to do with competence. In general case it is neither
    provable nor testable. Even more frequently it is merely economical
    viability of performing full analysis. The cost of failure it terms of recovery actions is negligible comparing to the cost of analysis and the
    risks that the analysis itself would be incorrect due to its complexity.

    And yet you think that
    same programmer is competent to write essentially untestable exception handling code without flaw, dealing with the vastly more complex
    situation of code that runs partially and stops unexpectedly.

    Absolutely. The handler works on a different context and any properly
    designed system has safe states defined for all such contexts. So, it
    just goes into that state.

    E.g. if the objective is to write into a database the result of
    computations and computations fail for whatever reason, the safe state
    could be defined as: write nothing, trace into the log, send a
    notification to the GUI.

    The whole concept of a "precondition" is nonsensical if the function
    implementation cannot rely on it being true!

    Which is why the precondition must be weakest possible. Thanks.

    From the function implementers viewpoint, it should be as /strong/ as possible.

    Their viewpoint is irrelevant. SW design is all about reuse.

    Correctness proofs and tests work together, not as alternatives.

    Nope. Applied to the *same* object, they are complementary.

    No, they work together.  Some things are best verified by proofs, some things by testing, some things by both.

    That is what complementary means.

    A /vastly/ better programmer
    than you or I said "Be careful of this code.  I have only proven it correct, not tested it."

    Knuth meant "not ran it," and informal meaning of testing. Both proofs
    and formal testing are done against the specifications. If
    specifications are erroneous, which is what the joke implies, then a
    test will show exactly what the proof had.

    We run software in order to apply rather common sense (and incomputable
    thing) than specifications. Specifications might say it is all OK, but
    when the customer see a piece of sh*t, he just says so. Then the
    programmer will answer, this what you wanted, idiot! (:-))

    However, I was not talking about testing the pre- or postconditions -
    I was talking about testing the exception handling code I, as the
    caller, have to write since the function may throw an exception.

    No, you have to write a test since the caller is not proven not to
    cause the function to propagate an exception. Again, what is the
    object here? What do you want to test? The caller, the callee, the
    compiler, the CPU?

    Or I could leave the exception unhandled,

    You cannot do that. Again, it is about proofs. If your subprogram
    has no exception in its postcondition you could not prove it correct
    unless x is non-negative.

    If you do not prove correctness, then exception propagation is just
    defined behavior as opposed to undefined one.

    This kind of misuse of exceptions to hide bugs in code is not about
    proofs - it is about arse-covering.

    No, it is about keeping contracts clean.

    What could be cleaner than the contract for a square root function that returns the square root of valid inputs?

    Nothing, and the contract I provided tell exactly this.

    Certainly not some drivel that
    pulls exception handling and all its types, hierarchies, handlers and mechanisms into the picture.

    No, the contract requires exception when inputs are mathematically
    valid. Big difference.

    It is impossible to define model types that would produce
    mathematically correct results. Start with + and produce types of X
    and Y such that the result is always mathematically correct X + Y.
    Exceptions is a method to fill the gaps between the model and the
    problem space without breaking the abstraction altogether.

    Oh wait, you claimed that the person writing the calling code
    couldn't possibly have forgotten to handle the exception, because
    then they wouldn't have been able to prove their code is correct.  So
    you trust them to have the skills, training, and resources to do
    formal correctness proofs on their code - but you don't trust them to
    be able to see if a value is negative?

    Yep, and this is trivially a halting problem:

        X := 2.0;
        if HALT(p) then
           X := -1.0;
        end if;
        X := sqrt (X);

    And, BTW, prover does proofs, not the developer.

    So you are not sure if the programmer might try to write a halting
    machine inside their function that calls square root?

    No, I am sure that what you suggested is impossible in general case.
    Now, feel free to enumerate all cases when that were possible and ship
    the manuscript to the programmers. They will certainly enjoy the reading...

    Okay, I suppose,
    if you are /that/ paranoid - but why should the person specifying or implementing the square root function have to deal with that?

    The person designing the contract must make it least possible burden for
    the user = remove nonsensical preconditions.

    Exceptions are a valid mechanism for handling unusual and rare
    run-time circumstances, such as a database transaction failure,
    loss of contact in a network connection, and so on.  They are /not/ >>>>> a crutch for poor programming and coding errors!

    Sure undefined behavior is much better than that! (:-))


    The point about undefined behaviour is that it does not happen.

    sqrt(-1.0)

    It is /better/ to leave situations that must not be allowed to occur,
    as undefined.

    sqrt(-1.0)

    The problem lies with the programmer that uses the function, or their development team or process that failed to catch the bug.  The function specifier is not responsible for that, and cannot fix the problem
    anyway.  So why make everything more complicated and higher risk?

    I fail to follow the logic:

    1. it is better to leave it undefined
    2. the programmer can run into the undefined
    3. this is less complicated and lowers the risk

    3 does not follow from 1 and 2.

    Do you really think it is realistic that in a real program, a mistake leading to an attempt at finding a negative square root is because
    someone wrote "sqrt(-1.0)", or perhaps "sqrt(-x)" instead of "sqrt(x)" ?

    I don't know if that is a mistake. sqrt is a part of some computational process running with some data. It could be perfectly valid, e.g. in an implementation of a calculator.

     No, the realistic source of the problem would be far back in the code
    - throwing an exception at this point will help very little.

    It is earliest possible detection that computation leaves the realm of
    real numbers. What does this mean in terms of problem space, I do not
    know and do not care if I am to implement real-valued sqrt.

    Do you
    really think the code would be able to recover sensibly and handle the situation gracefully when you throw an exception?

    Certainly so. Doing this all the time.

    No, that has no
    rooting in real life - at the point where the negative square root is called, the code's calculations and data are so screwed up that you
    won't be able to help.

    No. Calculations are a part of some large and complex system. You do not
    crash the aircraft because the board entertainment system failed to
    calculate the next volume adjustment step. Come on!

    With your specification, roles are mixed - should the caller check
    for negative x?

    Nothing in the contract of sqrt requires it. You are confusing
    contract parties. Without seeing the caller's contract nothing can be
    said.

    The specifications for the function are the contract.

    Yes and the specification is silent about any concrete values. The
    caller can call it with any instance of the type.

    Is that the function's job?

    The function's job is to implement the contract. How it does this is
    not the caller's business.

    Equally, the caller's job is to ensure that it fulfils /its/ side of the contract (the precondition).  How it does this is not the callee's business.

    Because there is no way a callee could control that. Which is why a
    contract is needed in the first place.

    If the caller has checked this, does it really have to handle an
    exception?

    It is not sqrt business what the caller does with the result. It is
    between the caller and the caller's caller.

    Indeed that is true.  And equally, it is not sqrt's business how the
    caller ensures that it has satisfied the precondition before calling it.

    No, it is an unnecessary burden on the caller:

    "If you do not do this and that, then I don't even know what I do then!
    You, you son of a b****, just wait, you..."

     The specified of sqrt has no interest in making a pointlessly weak precondition that forces a pointlessly complex postcondition.  Just let
    the caller ensure that it only passes non-negative numbers.

    The point is reducing undefined behavior. Ideally, there should be none
    in any program.

    How about debugging?  After all, real programmers make mistakes.
    With undefined behaviour, debugging tools (such as "sanitizer"
    options in modern compilers) can spot many kinds of undefined
    behaviour and give the developer a nice report about the problem.

    You fundamentally cannot detect undefined behavior, because any
    behavior can realize when undefined. It is allowed to fire a gamma jet
    that would roast the silly programmer...

    Yes.  But you can spot many types of undefined behaviour, both through static analysis and run-time checking in debug modes.

    I could or could not. But exception propagation is impossible to miss or ignore in a debugger. That is the difference.

    But your version doesn't have undefined behaviour - throwing an
    exception is part of the specification.

    Yep, and it can be trivially verified by tests or in any debugger.

    It is not a bug.  The debugger and tools don't know this defined
    behaviour is not intentional - because it is fully defined behaviour
    that the function caller can rely on.

    What, your debugger does not recognize exceptions? Come on!

    The responsibility for using appropriate inputs is where it should
    be - with the caller.

    Exactly, shoveling your problems to the caller, causing distributed
    overhead and making sure of having untraceable bugs in the
    production code under to motto:

        "no qualified programmer would ever do X"

    No, it is not passing on /your/ problems to anyone.  It is letting
    the caller take responsibility for /their/ code.

    "no qualified programmer would forget to check his code"

    Programmers can make mistakes - including the people calling sqrt, the people implementing it, and the people specifying it.  It makes sense to make life as clear and simple as practically possible for them.

    Yep, by following elementary design principles.

    When you give someone a recipe for merengues, you say "Beat the egg
    whites then slowly add the sugar while beating all the time in order to
    make the merengue mixture".  You don't add "alternatively, add the sugar then try to beat it in order to make a mess".

    I do not, the Universe does. In the physical world there is no such
    thing as undefined behavior. You can add salt, pepper, broken glass,
    anything and always get something. If that does not meet your
    expectations, read the instruction again!
    --
    Regards,
    Dmitry A. Kazakov
    http://www.dmitry-kazakov.de

    --- Synchronet 3.19c-Linux NewsLink 1.113
  • From Bart@bc@freeuk.com to comp.lang.misc on Tue Dec 6 11:38:44 2022
    From Newsgroup: comp.lang.misc

    On 06/12/2022 07:46, David Brown wrote:

    Why should you not faff around with "resize", "get_real_size" and all
    your other unnecessary extras?  Because it is is /slow/.  It is extra effort and library calls that slow down everything for so little
    potential benefit.

    Memory allocation is slow.  Freeing memory is slow.  You do it as rarely as you can, and you do it as locally as you can.  When you need
    something that might grow or shrink by small sizes, you track it locally
    in the data structure - your string, or dynamic array, or whatever, has
    a "size" and a "capacity" concept.  If "size" is already at "capacity"
    and you need to grow, you take a /big/ lump of memory from the next
    level, so that you won't have to ask for more any time soon.  At the library level you have an allocator that handles mid-sized lumps, from
    pools that are kept per thread.  When these run low, it asks for more
    from the OS.  And so on.

    Any time the size of something is dynamic, you ask for memory in
    generous lumps - usually in a power-of-two size for best alignment and
    easy fitting from the allocator,

    This is where your ideas converge, at least from mine. I use allocations
    in 'generous' lumps, but that is done within the allocator library.

    Hence the need to ask the library what the actual size was so that I can
    make use of the extra without needing to constantly call 'realloc'.

    The difference is that you want to bring all that extra work into the application, all that 'faffing' about rounding up (which is not so straightforward for very small or very large blocks).

    I want to keep as much of it within the allocator; all I need to know is
    that it /is/ generous.

    --- Synchronet 3.19c-Linux NewsLink 1.113
  • From Bart@bc@freeuk.com to comp.lang.misc on Tue Dec 6 12:13:31 2022
    From Newsgroup: comp.lang.misc

    On 06/12/2022 07:56, David Brown wrote:
    On 05/12/2022 23:25, James Harris wrote:
    #
    You can say it yet again but that doesn't mean it's true. Correctness
    is essential, of course, but for some programs which need hard
    real-time guarantees performance is equally as important.

    What part of "specific timing requirements are part of correctness" is giving you trouble here?  Other requirements can be part of correctness
    too - if the customer says the program must be written in Quick Basic,
    then a much faster and neater alternative in Turbo Pascal is /wrong/.

    It really is impossible to stress this too much.  Correctness is always
    key - it doesn't matter how efficient or performant an incorrect program is.  If you do not understand that, you are in the wrong profession.

    Sounds a nice idea, but what does correctness even mean within real,
    complex or interactive applications? Does it mean perfection? Does it
    apply to specification? Or practicality? Or usability? Or
    user-friendliness? Or safety? Or feature-coverage? Or interoperability?
    Or portability? Or the design of some API? Or scalability?

    How does it work with fault-tolerant programs, or can't those two
    concepts co-exist?

    How about correctness of quality: pixelisation, type hinting, aliasing, quantitisation artefacts? (This is a huge area.)

    How about correctness involving floating point, which necessarily will
    have some error?

    (I had a CAD product in which those small errors become signicant both
    at very small scales (eg. input to a CNC machine) and at very large ones
    (eg. mapping geographical features at 1:1 scale). Were problems
    connected with working at the limits of its abilties to do with incorrectness?)

    Notice I haven't even mentioned performance!

    It's too simplistic. Most software doesn't consist of some fantastic
    formula that you feed into a giant Multivac-style computer and it
    eventually emits a card with a single, correct number.

    I thought my experience in writing software was narrow; yours might be narrower!

    --- Synchronet 3.19c-Linux NewsLink 1.113
  • From David Brown@david.brown@hesbynett.no to comp.lang.misc on Tue Dec 6 13:38:03 2022
    From Newsgroup: comp.lang.misc

    On 06/12/2022 12:38, Bart wrote:
    On 06/12/2022 07:46, David Brown wrote:

    Why should you not faff around with "resize", "get_real_size" and all
    your other unnecessary extras?  Because it is is /slow/.  It is extra
    effort and library calls that slow down everything for so little
    potential benefit.

    Memory allocation is slow.  Freeing memory is slow.  You do it as
    rarely as you can, and you do it as locally as you can.  When you need
    something that might grow or shrink by small sizes, you track it
    locally in the data structure - your string, or dynamic array, or
    whatever, has a "size" and a "capacity" concept.  If "size" is already
    at "capacity" and you need to grow, you take a /big/ lump of memory
    from the next level, so that you won't have to ask for more any time
    soon.  At the library level you have an allocator that handles
    mid-sized lumps, from pools that are kept per thread.  When these run
    low, it asks for more from the OS.  And so on.

    Any time the size of something is dynamic, you ask for memory in
    generous lumps - usually in a power-of-two size for best alignment and
    easy fitting from the allocator,

    This is where your ideas converge, at least from mine. I use allocations
    in 'generous' lumps, but that is done within the allocator library.


    OK.

    Hence the need to ask the library what the actual size was so that I can make use of the extra without needing to constantly call 'realloc'.


    And here we diverge again.

    The difference is that you want to bring all that extra work into the application, all that 'faffing' about rounding up (which is not so straightforward for very small or very large blocks).

    I want to keep as much of it within the allocator; all I need to know is that it /is/ generous.


    No, I want it to be in the strings library here. And the strings
    library should know the size of the allocation blocks, because it is
    part of the same language standard library.

    There is absolutely /nothing/ to be gained by first calling "alloc",
    then "get_real_size", then "realloc" or "resize" to gain access to the
    extra space, compared to just calling "alloc" with the full size in the
    first place.

    It's not as if an allocator is sometimes going to dole out a round up to 32-byte block, sometimes rounding up to multiples of 26, or sometimes
    adding 7 extra bytes space. It will round up block sizes in a
    predictable fashion - and other library code can take advantage of that
    to get full blocks from the start.

    --- Synchronet 3.19c-Linux NewsLink 1.113
  • From David Brown@david.brown@hesbynett.no to comp.lang.misc on Tue Dec 6 13:59:51 2022
    From Newsgroup: comp.lang.misc

    On 06/12/2022 13:13, Bart wrote:
    On 06/12/2022 07:56, David Brown wrote:
    On 05/12/2022 23:25, James Harris wrote:
    #
    You can say it yet again but that doesn't mean it's true. Correctness
    is essential, of course, but for some programs which need hard
    real-time guarantees performance is equally as important.

    What part of "specific timing requirements are part of correctness" is
    giving you trouble here?  Other requirements can be part of
    correctness too - if the customer says the program must be written in
    Quick Basic, then a much faster and neater alternative in Turbo Pascal
    is /wrong/.

    It really is impossible to stress this too much.  Correctness is
    always key - it doesn't matter how efficient or performant an
    incorrect program is.  If you do not understand that, you are in the
    wrong profession.

    Sounds a nice idea, but what does correctness even mean within real,
    complex or interactive applications? Does it mean perfection? Does it
    apply to specification? Or practicality? Or usability? Or
    user-friendliness? Or safety? Or feature-coverage? Or interoperability?
    Or portability? Or the design of some API? Or scalability?


    These are excellent questions - and the answer is always "it depends".
    Writing good specifications is not easy - it is usually much harder than implementing them. And realistically, most specifications are not
    written formally or quantitatively. But that does not mean they are not
    part of the specifications, or that they are not part of correctness.

    So if the specification says "must be user-friendly even for beginners",
    then you need to find some beginners when testing the code to see if it
    meets that specification - if not, it is not correct.

    How does it work with fault-tolerant programs, or can't those two
    concepts co-exist?

    Get the specification right, and make the program right - then there are
    no faults to be tolerant of. (If your code has to be made to handle
    certain kinds of hardware fault, then those requirements would be part
    of the specification.)


    How about correctness of quality: pixelisation, type hinting, aliasing, quantitisation artefacts? (This is a huge area.)


    I covered that earlier. If you have certain flexibilities here, they go
    in the specification.

    To take a simpler but still realistic example, consider a function
    "sin". A rough specification might say "takes a double precision
    floating point number and returns its sine as a double". A more precise specification might say how NaNs are handled, or that they are not
    allowed. It might say something about setting errno (I personally think
    errno is a terrible idea, but some people like it). It should also say something about the precision of the result - such as a maximum error of ±1e-10 for inputs within ±8π and no requirements for inputs outside that range.

    For many uses, the specifications for "sin" will be "what the IEEE
    standard says" - and if you look there, it is quite details. For other
    uses - such as many of my uses in embedded systems - time limits are an important part of the specification, where as accuracy requirements are
    much more relaxed.

    How about correctness involving floating point, which necessarily will
    have some error?

    Error limits are part of the specifications.


    (I had a CAD product in which those small errors become signicant both
    at very small scales (eg. input to a CNC machine) and at very large ones (eg. mapping geographical features at 1:1 scale). Were problems
    connected with working at the limits of its abilties to do with incorrectness?)

    Notice I haven't even mentioned performance!

    It's too simplistic. Most software doesn't consist of some fantastic
    formula that you feed into a giant Multivac-style computer and it
    eventually emits a card with a single, correct number.

    I thought my experience in writing software was narrow; yours might be narrower!


    I think in a lot of cases you /have/ been focusing on correctness, you
    just didn't what correctness entials. And in many other cases, you have
    done so by changing the specifications on the fly as you go along. But
    in other cases, you have no doubt been focusing on the wrong things,
    because they are more fun, or easier, or appear more impressive.
    (That's not a personal thing - /everyone/ does that.)

    And I am not saying that things like performance, maintainability, user-friendliness, etc., are not important - I am saying that they
    always come second to /correctness/.

    So if the correct operation of the program requires calculating a length
    to 0.1 mm accuracy within 3 seconds, a simple one-button interface or an elegant 3 line program will not be good enough if those correctness
    criteria are not fulfilled.
    --- Synchronet 3.19c-Linux NewsLink 1.113
  • From David Brown@david.brown@hesbynett.no to comp.lang.misc on Tue Dec 6 14:57:13 2022
    From Newsgroup: comp.lang.misc

    On 06/12/2022 12:22, Dmitry A. Kazakov wrote:
    On 2022-12-05 23:29, David Brown wrote:
    On 05/12/2022 21:39, Dmitry A. Kazakov wrote:
    On 2022-12-05 17:56, David Brown wrote:
    On 05/12/2022 16:14, Dmitry A. Kazakov wrote:
    On 2022-12-05 15:38, David Brown wrote:
    On 05/12/2022 12:06, Dmitry A. Kazakov wrote:
    On 2022-12-05 11:40, David Brown wrote:

    If you specify your square root function with a precondition of
    "true", then no input is erroneous.

    No. It is so that input is a caller's bug = contract violation.
    Negative input is an erroneous, exceptional input. No difference to
    inputs causing + to overflow. Care to write a strong precondition for
    +, *, /, gamma etc?

    int add_int(int a, int b);
    precondition: (a + b >= INT_MIN) && (a + b <= INT_MAX)

    It's not rocket science.

    It is! Since the precondition here is not written in the object
    language. Remember, you argued that it is all for the caller's benefit.

    The specification - the precondition and the postcondition - are for
    both the caller's and the callee's benefits.

    How the caller is supposed to check it? To learn, so to say, the full
    scale of benefits bestowed on it? (:-))

    Anyway, I presume it is finest design when X+Y returns garbage on
    overflow, underflow etc?

    No matter what you do, or how you specify things, if you have two
    integers of restricted range and add them together, you will not be able
    to get the actual mathematically correct answer when you go out of
    range. I don't think anyone can argue against that.

    It doesn't matter what you do - wrap them, saturate them, throw errors,
    return 0 - it's all wrong. A function specified to take two 32-bit
    integers and return their sum as a 32-bit integer is unimplementable.

    So why go so far out of your way to make a solution that is
    inconvenient, complicated, highly inefficient, untestable, restrictive
    for the rest of the code, blocks or limits correctness proofs - and is
    /still/ wrong?

    It is much better to pick the simplest and cleanest solution, and make
    the requirements for use (the precondition) clear and explicit.

    And yet you want to specify what a square root function should do when
    given this mathematically unworkable input.

    Absolutely. Square root must indicate this unfortunate state. Again what else it should do? Format the hard drive?


    The square root function is not responsible for people misusing it, or
    the consequences of that.

    string path
    float y
    try {
    path = "/"
    y = sqrt(x)
    path += "%0.2f" % y
    log("Preparing to delete...")
    } except {
    std_error("Failed to write to log!")
    }
    delete_recursive(path)

    So what happens in the unfortunate situation of negative "x" ? The
    filessytem gets wiped - and that is /despite/ the programmer thinking
    they have coded an appropriate exception handler for the code they think
    can have throw an exception.

    Of course you can say the language should distinguish between exception
    types or give other aid, but you can always make more sophisticated
    examples. The principle is that when something has gone wrong, there is
    no inherent limit as to how bad the consequences could be. The
    implementer of sqrt does not know you are using the result to control
    deletion of files - all he or she really knows about is how to calculate square roots, and that is all that is needed. The rest is someone
    else's responsibility.


    The point is reducing undefined behavior. Ideally, there should be none
    in any program.


    Of course I agree here. If you don't call my sqrt with a negative
    number, there is no undefined behaviour. It's simple.

    How about debugging?  After all, real programmers make mistakes.
    With undefined behaviour, debugging tools (such as "sanitizer"
    options in modern compilers) can spot many kinds of undefined
    behaviour and give the developer a nice report about the problem.

    You fundamentally cannot detect undefined behavior, because any
    behavior can realize when undefined. It is allowed to fire a gamma
    jet that would roast the silly programmer...

    Yes.  But you can spot many types of undefined behaviour, both through
    static analysis and run-time checking in debug modes.

    I could or could not. But exception propagation is impossible to miss or ignore in a debugger. That is the difference.

    But your version doesn't have undefined behaviour - throwing an
    exception is part of the specification.

    Yep, and it can be trivially verified by tests or in any debugger.

    It is not a bug.  The debugger and tools don't know this defined
    behaviour is not intentional - because it is fully defined behaviour
    that the function caller can rely on.

    What, your debugger does not recognize exceptions? Come on!


    Exceptions - used correctly - are part of normal code flow. I don't
    want my debugger to stop because of an exception, I want it to flow
    through the exceptions and run the exception handling code!


    When you give someone a recipe for merengues, you say "Beat the egg
    whites then slowly add the sugar while beating all the time in order
    to make the merengue mixture".  You don't add "alternatively, add the
    sugar then try to beat it in order to make a mess".

    I do not, the Universe does. In the physical world there is no such
    thing as undefined behavior. You can add salt, pepper, broken glass, anything and always get something. If that does not meet your
    expectations, read the instruction again!


    I don't think you understand what the term "undefined behaviour" means.
    Many people don't, even when they are seasoned programmers - and that
    is why they fear it.

    It simply means the specifications don't say anything about the given circumstances.

    So your /implementation/ of sqrt - throwing an exception on negative
    inputs - is likely to be a perfectly good implementation for /my/ specification. It will be a problem only if the exception is considered
    part of the function type in the language in use, or forces the
    programmer to handle it in some way. I don't know the details of
    exceptions in Ada, but in C++ it would be fine.

    Alternatively, you can write :

    float sqrt(float x) {
    if (x < 0) {
    send_email_to_callers_boss();
    } else {
    return real_square_root(x);
    }
    }

    Or it could have "breakpoint_here()", or anything else you like. It
    could be dependent on whether the program is run with a "--testing"
    option.

    Or it could ignore the possibility entirely.


    --- Synchronet 3.19c-Linux NewsLink 1.113
  • From David Brown@david.brown@hesbynett.no to comp.lang.misc on Tue Dec 6 15:08:16 2022
    From Newsgroup: comp.lang.misc

    On 06/12/2022 09:43, Dmitry A. Kazakov wrote:
    On 2022-12-06 00:47, James Harris wrote:

    Rather than a contract (which can lead to an error if the precondition
    is not met) it may be better to /define/ the range of possible inputs
    to a function: a call which has parameters which do not meet the
    definitions will not even match with the function which cannot handle
    them. For example,

       sqrt(-4.0)

    would not match with a definition

       function sqrt(real value :: value >= 0) -> real

    but would match with

       function sqrt(real value) -> complex


    Yes, those are fine.


    It is untyped.

    Bollocks.

    The formal proof is trivial, I gave it already:

       X := 4.0;
       if HALT(p) then
          X := -4.0;
       end if;
       sqrt (X); -- What is the type of?

    Bollocks.

    This not a "proof" of anything, it is not "formal", it does not mean "it
    is all equivalent to the halting problem" or anything of the sort.
    There is no particular reason to assume anything about "HALT(p)", since
    "p" is not defined - it could well be perfectly clear whether "p" halts
    or not. And if the choice of "p" is such that "HALT(p)" is not
    computable, then the whole thing is not going to be a legal program and
    thus meaningless. (If you think it is a valid program, show your implementation of "HALT". A syntactically valid piece of code does not
    a legal program make.)


    The type of "X" here is a /signed/ floating point. The /value/ of X
    might be hard to determine - it is either 4.0 or -4.0 - but the /type/
    is clear.


    The type is undecidable in a legal program, ergo, types are broken.

    The general point is. You cannot map *all* correctness onto types. Types
    are too weak for that. You cannot express *all* behavior in terms of
    types. The example with sqrt perfectly illustrates that.


    The general point is that you cannot use compile-time or static analysis
    to verify all aspects of run-time behaviour, at least assuming there is
    some input to the program. If you start with a real number "x" of
    unknown signedness, you cannot convert it into a type that is limited to non-negative real numbers without some kind of checking and limiting
    while staying within the specifications of the types.
    --- Synchronet 3.19c-Linux NewsLink 1.113
  • From David Brown@david.brown@hesbynett.no to comp.lang.misc on Tue Dec 6 15:09:50 2022
    From Newsgroup: comp.lang.misc

    On 06/12/2022 00:36, James Harris wrote:
    On 05/12/2022 14:38, David Brown wrote:

    ...

    Exceptions are a valid mechanism for handling unusual and rare
    run-time circumstances, such as a database transaction failure, loss
    of contact in a network connection, and so on.  They are /not/ a
    crutch for poor programming and coding errors!

    That's one school of thought. Another is that exceptions be used to
    indicate also errors which a programmer would not normally want to test
    for each time they can occur so that his code can focus on the logic of
    the task at hand, e.g. OOM in

      p = memory_allocate(SIZE);

    or IO error from

      nbytes = read(fd, &buf, BUFSIZE);



    These are examples of exactly the sort of thing I was talking about.

    --- Synchronet 3.19c-Linux NewsLink 1.113
  • From David Brown@david.brown@hesbynett.no to comp.lang.misc on Tue Dec 6 15:19:30 2022
    From Newsgroup: comp.lang.misc

    On 05/12/2022 23:38, James Harris wrote:
    On 04/12/2022 23:12, David Brown wrote:
    On 04/12/2022 15:07, James Harris wrote:
    On 04/12/2022 13:27, David Brown wrote:

    ...

    I thought that was what you and Bart (who IIRC also mentioned
    something similar) had in mind and I spend some time designing an
    allocator to support splitting memory in that way down to the byte
    level. It was an interesting exercise and I may use it one day but I
    thought you had some use cases in mind.

    Who cares?  Launch the nasal daemons.

    Never, never, and never!


    Garbage in, garbage out.

    You cannot give the program sensible answers to stupid questions.  And
    no matter how much you try to check the program's requests for sanity
    and valid values, there will be a programmer who will outwit you and
    find new ways to be wrong that are beyond your imagination.  If you
    have a low-level language that allows things like pointers and
    conversions between points and integers, this will happen /very/
    easily, and there is nothing you can do to stop it.

    ...

    The concept of undefined behaviour - of "garbage in, garbage out" - is
    as old as computing:

    Garbage out is not the same as undefined behaviour. Here's an example of garbage in leading to garbage out

      Enter your age: 1000
      You owe us $-1,234,567

    Here's an example of UB

      Enter your age: 1000
      Deleting files in your home directory


    Nasal demons should never be permitted.



    No.

    An example of undefined behaviour is :

    Enter your age between 0 and 200, and I will convert it to Mars years.

    No one has said what will happen here if you enter 1000. The developer
    can do whatever he or she likes in such cases, including ignoring it
    entirely, or converting it accurately to Mars years.

    The precondition to the program is simple and clear - it is /your/ fault
    if you put in an age outside the range.

    The designer of a saucepan says what will happen if you put it on a
    normal cooker, or in a dishwasher, or in a home oven - because that is expected use of the saucepan. He does not say what will happen when you
    throw it in a blast furnace, or put it on your head and dance naked in
    the street - those are undefined behaviours. Should those possibilities
    have been included in the instruction manual for the saucepan? Should
    the saucepan manufacturer take responsibility for knowing the local laws
    where you live, and determining if you'll be arrested, locked in a
    padded cell, freeze to death, or simply have a refreshing dance in your
    own garden?

    So why should the implementer of sqrt consider all the ways you could
    attempt to use the function outside of its specification?
    --- Synchronet 3.19c-Linux NewsLink 1.113
  • From Dmitry A. Kazakov@mailbox@dmitry-kazakov.de to comp.lang.misc on Tue Dec 6 17:31:28 2022
    From Newsgroup: comp.lang.misc

    On 2022-12-06 15:08, David Brown wrote:
    On 06/12/2022 09:43, Dmitry A. Kazakov wrote:
    On 2022-12-06 00:47, James Harris wrote:

    would not match with a definition

       function sqrt(real value :: value >= 0) -> real

    but would match with

       function sqrt(real value) -> complex

    Yes, those are fine.

    It is untyped.

    Bollocks.

    The formal proof is trivial, I gave it already:

        X := 4.0;
        if HALT(p) then
           X := -4.0;
        end if;
        sqrt (X); -- What is the type of?

    Bollocks.

    [...]

    The type of "X" here is a /signed/ floating point.

    The type of sqrt(x)! Read carefully what James suggested.

    To James. As a general rule. The signature may not depend on the actual
    values of the arguments. There are ways to wiggle out of the situation
    when everything collapses into a single god-type, like complex in this
    case. You can introduce constraints and try to check them statically,
    when possible, while keeping different representations of statically constrained object, e.g. real-only complex values. But that is far
    beyond the type system of the language you are envisioning. So, a
    standard way is to stick to independent types real and complex and
    having incomplete sqrt, exponentiation etc for real.
    --
    Regards,
    Dmitry A. Kazakov
    http://www.dmitry-kazakov.de

    --- Synchronet 3.19c-Linux NewsLink 1.113
  • From Dmitry A. Kazakov@mailbox@dmitry-kazakov.de to comp.lang.misc on Tue Dec 6 17:33:46 2022
    From Newsgroup: comp.lang.misc

    On 2022-12-06 14:57, David Brown wrote:
    On 06/12/2022 12:22, Dmitry A. Kazakov wrote:
    On 2022-12-05 23:29, David Brown wrote:
    On 05/12/2022 21:39, Dmitry A. Kazakov wrote:
    On 2022-12-05 17:56, David Brown wrote:
    On 05/12/2022 16:14, Dmitry A. Kazakov wrote:
    On 2022-12-05 15:38, David Brown wrote:
    On 05/12/2022 12:06, Dmitry A. Kazakov wrote:
    On 2022-12-05 11:40, David Brown wrote:

    If you specify your square root function with a precondition of
    "true", then no input is erroneous.

    No. It is so that input is a caller's bug = contract violation.
    Negative input is an erroneous, exceptional input. No difference to
    inputs causing + to overflow. Care to write a strong precondition
    for +, *, /, gamma etc?

    int add_int(int a, int b);
    precondition: (a + b >= INT_MIN) && (a + b <= INT_MAX)

    It's not rocket science.

    It is! Since the precondition here is not written in the object
    language. Remember, you argued that it is all for the caller's benefit.

    The specification - the precondition and the postcondition - are for
    both the caller's and the callee's benefits.

    How the caller is supposed to check it? To learn, so to say, the full
    scale of benefits bestowed on it? (:-))

    You did not answer the question.

    Anyway, I presume it is finest design when X+Y returns garbage on
    overflow, underflow etc?

    No matter what you do, or how you specify things, if you have two
    integers of restricted range and add them together, you will not be able
    to get the actual mathematically correct answer when you go out of
    range.  I don't think anyone can argue against that.

    Except you, who presumes that the mathematically correct answer must be
    given and moreover it is somehow a responsibility of the caller.

    So why go so far out of your way to make a solution that is
    inconvenient, complicated, highly inefficient, untestable, restrictive
    for the rest of the code, blocks or limits correctness proofs - and is /still/ wrong?

    Because it is

    - convenient, since logical errors get detected

    - incomplicated, since consistency checks are easier for the
    implementation. In many cases they are need not be explicit.

    - highly efficient, because the caller need not to check anything in
    advance and because the callee's checks might be free, depending on the algorithm and the hardware.

    - trivially testable per normal coverage tests

    - impose no restrictions on the caller

    It is much better to pick the simplest and cleanest solution, and make
    the requirements for use (the precondition) clear and explicit.

    Which always is the weakest possible precondition.

    And yet you want to specify what a square root function should do
    when given this mathematically unworkable input.

    Absolutely. Square root must indicate this unfortunate state. Again
    what else it should do? Format the hard drive?

    The square root function is not responsible for people misusing it, or
    the consequences of that.

    You did not answer.

        string path
        float y
        try {
            path = "/"
            y = sqrt(x)
            path += "%0.2f" % y
            log("Preparing to delete...")
        } except {
            std_error("Failed to write to log!")
        }
        delete_recursive(path)

    So what happens in the unfortunate situation of negative "x" ?  The filessytem gets wiped - and that is /despite/ the programmer thinking
    they have coded an appropriate exception handler for the code they think
    can have throw an exception.

    Why do you give an evidently broken code as an example? Use pre- and postconditions! What is the precondition to the call to
    delete_recursive? It is that the path were correct. Did you satisfy it?
    No, you deliberately broke it.

    [catching all exceptions is great no-no in every code guideline]

    Anyway, how, *not* raising exception and assigning garbage to y should
    cure your broken code?

    Of course you can say the language should distinguish between exception types or give other aid, but you can always make more sophisticated examples.

    Nope. I say, write proper code.

    But your version doesn't have undefined behaviour - throwing an
    exception is part of the specification.

    Yep, and it can be trivially verified by tests or in any debugger.

    It is not a bug.  The debugger and tools don't know this defined
    behaviour is not intentional - because it is fully defined behaviour
    that the function caller can rely on.

    What, your debugger does not recognize exceptions? Come on!

    Exceptions - used correctly - are part of normal code flow.  I don't
    want my debugger to stop because of an exception, I want it to flow
    through the exceptions and run the exception handling code!

    So, the debugger recognizes them, but you prefer to ignore all warnings.

    When you give someone a recipe for merengues, you say "Beat the egg
    whites then slowly add the sugar while beating all the time in order
    to make the merengue mixture".  You don't add "alternatively, add the
    sugar then try to beat it in order to make a mess".

    I do not, the Universe does. In the physical world there is no such
    thing as undefined behavior. You can add salt, pepper, broken glass,
    anything and always get something. If that does not meet your
    expectations, read the instruction again!

    I don't think you understand what the term "undefined behaviour" means.
     Many people don't, even when they are seasoned programmers - and that
    is why they fear it.

    It simply means the specifications don't say anything about the given circumstances.

    No, it means exactly what it says: the program's behavior is undefined,
    it can do absolutely anything = be in any possible state. The state of
    the Universe is always confined and variation of states are always
    localized in space and time. Which is why engineering of hardware and programming are so different.

    So your /implementation/ of sqrt - throwing an exception on negative
    inputs - is likely to be a perfectly good implementation for /my/ specification.

    Yes, because your specification was poorly designed allowing anything
    when argument is negative.

    It will be a problem only if the exception is considered
    part of the function type in the language in use, or forces the
    programmer to handle it in some way.

    No, that is exactly the intent: to force the programmer to take care
    about all possible states, not just those he fancies. Lazy programmers,
    and that is about all of them, have tunnel vision.

    As an old programmer's joke says: never test for things that may
    actually fail... (:-))
    --
    Regards,
    Dmitry A. Kazakov
    http://www.dmitry-kazakov.de

    --- Synchronet 3.19c-Linux NewsLink 1.113
  • From David Brown@david.brown@hesbynett.no to comp.lang.misc on Tue Dec 6 18:16:40 2022
    From Newsgroup: comp.lang.misc

    On 06/12/2022 17:33, Dmitry A. Kazakov wrote:
    On 2022-12-06 14:57, David Brown wrote:
    On 06/12/2022 12:22, Dmitry A. Kazakov wrote:
    On 2022-12-05 23:29, David Brown wrote:
    On 05/12/2022 21:39, Dmitry A. Kazakov wrote:
    On 2022-12-05 17:56, David Brown wrote:
    On 05/12/2022 16:14, Dmitry A. Kazakov wrote:
    On 2022-12-05 15:38, David Brown wrote:
    On 05/12/2022 12:06, Dmitry A. Kazakov wrote:
    On 2022-12-05 11:40, David Brown wrote:

    If you specify your square root function with a precondition of
    "true", then no input is erroneous.

    No. It is so that input is a caller's bug = contract violation.
    Negative input is an erroneous, exceptional input. No difference to >>>>> inputs causing + to overflow. Care to write a strong precondition
    for +, *, /, gamma etc?

    int add_int(int a, int b);
    precondition: (a + b >= INT_MIN) && (a + b <= INT_MAX)

    It's not rocket science.

    It is! Since the precondition here is not written in the object
    language. Remember, you argued that it is all for the caller's benefit.

    The specification - the precondition and the postcondition - are for
    both the caller's and the callee's benefits.

    How the caller is supposed to check it? To learn, so to say, the full
    scale of benefits bestowed on it? (:-))

    You did not answer the question.

    The answer is - as usual - "it depends". If the caller knows enough
    about the values "a" and "b", then there is no need for any kind of
    run-time check. That's by far the most common case. If the caller
    knows nothing about the values, and they could be anything from
    anywhere, then it's probably time to sanitise them in some way - which
    all depends on what they are.

    The last thing I'd want to do is throw them into a sum and hope for the
    best.


    Anyway, I presume it is finest design when X+Y returns garbage on
    overflow, underflow etc?

    No matter what you do, or how you specify things, if you have two
    integers of restricted range and add them together, you will not be
    able to get the actual mathematically correct answer when you go out
    of range.  I don't think anyone can argue against that.

    Except you, who presumes that the mathematically correct answer must be given and moreover it is somehow a responsibility of the caller.


    I presume the mathematically correct answer will be given by the
    function "add" if the inputs are valid for the precondition.

    What I really do not want is a situation where the function claims the
    inputs are valid since the precondition evaluates to "true", and yet the function is /not/ giving me a mathematically correct answer. How am I supposed to rely on a schizophrenic function like that? If you don't
    like my inputs, use the precondition to say you don't like them. That's
    what it is for.


    So why go so far out of your way to make a solution that is
    inconvenient, complicated, highly inefficient, untestable, restrictive
    for the rest of the code, blocks or limits correctness proofs - and is
    /still/ wrong?

    Because it is

    - convenient, since logical errors get detected

    It is inconvenient for the caller - the function doesn't do what I want,
    and forces me to go to extra effort dealing with imaginary exceptions
    and errors even when I know they won't be triggered.

    It is inconvenient for the implementer, who has to check for situations outside the function's control, doing extra work that has nothing to do
    with the function's purpose, all to handle someone else's mistakes.


    - incomplicated, since consistency checks are easier for the
    implementation. In many cases they are need not be explicit.


    See above.

    - highly efficient, because the caller need not to check anything in
    advance and because the callee's checks might be free, depending on the algorithm and the hardware.


    No, it is massively inefficient. Instead of a single assembly
    instruction, or perhaps being pre-calculated at compile time, you now
    have extra checks, jumps, exception throw calls, stack frames holding exception data, and $DEITY knows what else. And the compiler can no
    longer optimise expressions such as "a + b - a" to "b", since perhaps "a
    + b" would throw a tantrum.

    - trivially testable per normal coverage tests


    When the calling code is correct, the exception is never thrown. So the exception handling code you now have to put in is never tested - unless
    you first break the calling code.

    - impose no restrictions on the caller

    The caller now has to deal with an expression which could throw an
    exception, instead of a simple addition.


    It is much better to pick the simplest and cleanest solution, and make
    the requirements for use (the precondition) clear and explicit.

    Which always is the weakest possible precondition.

    I really don't comprehend why you still think this. It is just /wrong/.

    This stuff is basic computer science.


    Why do you give an evidently broken code as an example? Use pre- and postconditions! What is the precondition to the call to
    delete_recursive? It is that the path were correct. Did you satisfy it?
    No, you deliberately broke it.

    So now the precondition for the "delete_recursive" function is supposed
    to be reading the developer's mind and guessing what they were intended
    to do? Isn't that a bit, well, /strong/ ?

    I recommend you learn what preconditions actually /are/, what the mean,
    and how they are used to develop software clearly and efficiently. Recommending pointless "true" preconditions on functions that need them,
    along with magic preconditions on those that don't, is hardly an optimal
    way to write software.


    I doubt if we are ever going to agree here. Maybe it's time to move on?
    --- Synchronet 3.19c-Linux NewsLink 1.113
  • From Dmitry A. Kazakov@mailbox@dmitry-kazakov.de to comp.lang.misc on Tue Dec 6 18:29:38 2022
    From Newsgroup: comp.lang.misc

    On 2022-12-06 18:16, David Brown wrote:

    I doubt if we are ever going to agree here.  Maybe it's time to move on?

    Yes, everything important has been said already.
    --
    Regards,
    Dmitry A. Kazakov
    http://www.dmitry-kazakov.de

    --- Synchronet 3.19c-Linux NewsLink 1.113
  • From Bart@bc@freeuk.com to comp.lang.misc on Tue Dec 6 19:29:20 2022
    From Newsgroup: comp.lang.misc

    On 06/12/2022 14:09, David Brown wrote:
    On 06/12/2022 00:36, James Harris wrote:
    On 05/12/2022 14:38, David Brown wrote:

    ...

    Exceptions are a valid mechanism for handling unusual and rare
    run-time circumstances, such as a database transaction failure, loss
    of contact in a network connection, and so on.  They are /not/ a
    crutch for poor programming and coding errors!

    That's one school of thought. Another is that exceptions be used to
    indicate also errors which a programmer would not normally want to
    test for each time they can occur so that his code can focus on the
    logic of the task at hand, e.g. OOM in

       p = memory_allocate(SIZE);

    or IO error from

       nbytes = read(fd, &buf, BUFSIZE);



    These are examples of exactly the sort of thing I was talking about.


    To me they are examples of what /I/ was talking about with
    case-sensitive code.

    Namely, reading a bit of normal-looking code then encountering something
    in ALLCAPS for no reason that is relevant to anyone reading the code.

    (It's also a example of that strange habit of dropping a repeated letter
    in a contraction, like using 'bufsize' instead of 'buffsize' when
    'buffersize' is too long to type.)

    --- Synchronet 3.19c-Linux NewsLink 1.113
  • From David Brown@david.brown@hesbynett.no to comp.lang.misc on Tue Dec 6 20:39:11 2022
    From Newsgroup: comp.lang.misc

    On 06/12/2022 20:29, Bart wrote:
    On 06/12/2022 14:09, David Brown wrote:
    On 06/12/2022 00:36, James Harris wrote:
    On 05/12/2022 14:38, David Brown wrote:

    ...

    Exceptions are a valid mechanism for handling unusual and rare
    run-time circumstances, such as a database transaction failure, loss
    of contact in a network connection, and so on.  They are /not/ a
    crutch for poor programming and coding errors!

    That's one school of thought. Another is that exceptions be used to
    indicate also errors which a programmer would not normally want to
    test for each time they can occur so that his code can focus on the
    logic of the task at hand, e.g. OOM in

       p = memory_allocate(SIZE);

    or IO error from

       nbytes = read(fd, &buf, BUFSIZE);



    These are examples of exactly the sort of thing I was talking about.


    To me they are examples of what /I/ was talking about with
    case-sensitive code.

    Namely, reading a bit of normal-looking code then encountering something
    in ALLCAPS for no reason that is relevant to anyone reading the code.


    I would never have used ALLCAPS myself here, but it was not something
    that I thought about in this bit of the code. (I can understand having all-caps for "dangerous" macros that do something weird, but not for
    something that's just a constant. That's ugly in my view.)

    (It's also a example of that strange habit of dropping a repeated letter
    in a contraction, like using 'bufsize' instead of 'buffsize' when 'buffersize' is too long to type.)


    Do you think that's a general rule or common habit? I shall try to pay attention, and see if it is a pattern in code I come across. Maybe it
    is just that "buf" is an abbreviation for "buffer", while "buff" is a different word entirely.



    --- Synchronet 3.19c-Linux NewsLink 1.113
  • From James Harris@james.harris.1@gmail.com to comp.lang.misc on Wed Dec 7 16:20:50 2022
    From Newsgroup: comp.lang.misc

    On 06/12/2022 07:46, David Brown wrote:
    On 05/12/2022 23:08, James Harris wrote:
    On 04/12/2022 22:23, David Brown wrote:
    On 04/12/2022 14:54, James Harris wrote:
    On 04/12/2022 12:31, David Brown wrote:

    ...

    Look, at the lowest level, buffers are /never/ enlarged.

    Rubbish! Just because /you/ never want such a thing does not mean
    that others do not want it, nor does your dislike for it or your
    lack of experience of it mean that it's a bad idea.


    They are /not/ enlarged.

    On the contrary, in what I proposed buffers /would be/ enlarged. You
    are evidently arguing against my proposal when you don't understand
    it. Please put aside whatever model you have in mind and consider the
    proposal if you want to raise objections to it!! We've been arguing
    about this for over a week, now, and it's not a good use of time for
    objections to be unrelated to the proposal they are supposed to be
    against. :-(


    You allocate some memory for a buffer.  You allocate more memory
    later for something else - it gets put after the first buffer.  How
    are you going to enlarge the first buffer?  Stomp all over the second
    allocation?

    I cannot work out what model you have in mind but it doesn't seem to
    match what I proposed. To illustrate, say there is a chain of two
    buffers:

       buffer A is 1000 bytes
       buffer B is 100 bytes

    A string already occupies all of A and B. The application tells the
    string library to append 80 bytes to the string. There is no room in B
    so the string library asks the memory allocator to resize B in place
    to make it large enough. Let's say the allocator finds 20 bytes of
    free space after B but not the 80 asked for. It makes B 20 bytes
    longer. As it's not long enough the string library then requests space
    for another buffer, C, and add it to the chain; it would then put 20
    bytes in B and 60 in C.

    ...

    to squeeze in a few extra bytes.  The rest is done by a /new/
    allocation, not an enlargement.  That means all the time spend trying to figure out if you have an extra byte or two is wasted.

    Your understanding of the proposal may be improving but there's some way
    to go. The resize call would not be limited to padding for alignment but
    could return a *lot* more if a lot more were available.

    Maybe it's the small memory requirements I used in the example which is misleading you. They were just illustrative. Say, instead, an existing allocation, D, is 400k and a library using that space wanted to extend
    it by 100k bytes. It would call

    resize(D, 500_000)

    If there was enough space after D for it to be extended to 500k then
    that's what would happen. The size of D would be increased to (at least)
    500k without moving the initial 400k to new memory.

    ...

    There are basically two situations for strings.  One case is that you
    know its size - it's a literal, or immutable object.  Allocate one
    single buffer big enough to hold it, and you're done.  No enlargement.

    Of course.


    The other case is that it is unknown in size.  The simple handling is to use "realloc" as needed, growing as necessary.  That's going to be very efficient for a lot of cases, but poor for big strings.

    Indeed. That's what Bart did and is something I've done in the past but
    it can still slow down on large data.

    Or allocate
    buffers appropriately as it grows, keeping a chain.

    Yes, as I've said before, if the string length was unknown I would
    expect the string library to begin a chain of buffers. It is the only
    solution which truly scales. But note the difference:

    With your solution of buffers all of the same size you would need strlen
    / bufsize buffers. Each buffer would contain a 'run' of characters and
    you could end up with many buffers. Alternatively, if the buffers were
    large you'd end up with much internal fragmentation.

    With my solution of allowing the allocator to extend buffers where
    there's space available you'd normally need fewer buffers, and if enough
    space were available where the first buffer was allocated the whole
    string would be contained in a single run.

    ...

    If your string library already knows the size of allocation lumps, there
    is never any point in calling "resize" - it will never give you
    anything.  You want to make the basic "string" structure a neat size and with enough space for short strings (perhaps 12 - 20 characters), but
    you make the lumps it allocate sized to be multiples of your basic allocation size.

    That shows me that you misunderstand what resize() would do. Hopefully
    what I've written above will have made it clearer. Hopefully!
    --
    James Harris


    --- Synchronet 3.19c-Linux NewsLink 1.113
  • From James Harris@james.harris.1@gmail.com to comp.lang.misc on Wed Dec 7 16:37:03 2022
    From Newsgroup: comp.lang.misc

    On 06/12/2022 07:56, David Brown wrote:
    On 05/12/2022 23:25, James Harris wrote:
    On 04/12/2022 22:59, David Brown wrote:
    On 04/12/2022 19:21, James Harris wrote:

    ...

    The case in point is two libraries: a memory allocator and a string
    library which uses the memory allocator. What I am saying to you is
    that such libraries need to be correct but they also need to scale
    to large strings, many strings, many string operations, many other
    non-string allocation activity, etc, because it's impossible to say
    just now how they will be used. It's no good to just "focus on
    correctness".


    I did not say that you should ignore performance or efficiency.  I
    said /correctness/ trumps performance every time.  /Every/ time.  No
    exceptions.

    You can say it yet again but that doesn't mean it's true. Correctness
    is essential, of course, but for some programs which need hard
    real-time guarantees performance is equally as important.

    What part of "specific timing requirements are part of correctness" is giving you trouble here?  Other requirements can be part of correctness
    too - if the customer says the program must be written in Quick Basic,
    then a much faster and neater alternative in Turbo Pascal is /wrong/.

    None. I indicated agreement with that statement.


    It really is impossible to stress this too much.  Correctness is always
    key - it doesn't matter how efficient or performant an incorrect program is.  If you do not understand that, you are in the wrong profession.

    Which part of "Correctness is essential" is giving you trouble? ;-)




    If particular specific performance expectations are part of the
    requirements, then they are part of /correctness/.

    Indeed.


    If you are just looking for "as fast as practically possible given
    the budget constraints for the development process", then that's fine
    too - but it /always/ comes secondary to correctness.

    That is at least a development on what you said before but it is still
    impractical. If you implemented a simple n-squared sort "due to budget
    constraints" it may work fine for weeks but then hang a system which
    got hit with a much larger data set. It does not scale. I dread to
    think how much 'professional' and paid-for software has followed
    similar principles but I've been unfortunate enough to use some of it.


    The problem is not from programmers focusing on correctness.  The main problem in software development is failures at the specification level.

    Well, formal specifications are more relevant for end-user apps but
    someone writing middleware will not now how the software might be used
    in future. It is then important to anticipate potential future uses. As
    well as correctness such software needs to /scale/.
    --
    James Harris


    --- Synchronet 3.19c-Linux NewsLink 1.113
  • From Dmitry A. Kazakov@mailbox@dmitry-kazakov.de to comp.lang.misc on Wed Dec 7 17:46:19 2022
    From Newsgroup: comp.lang.misc

    On 2022-12-07 17:37, James Harris wrote:

    Well, formal specifications are more relevant for end-user apps but
    someone writing middleware will not now how the software might be used
    in future.

    As a designer of a middleware I can tell you that for it specifications
    are even more important.

    It is then important to anticipate potential future uses. As
    well as correctness such software needs to /scale/.

    I see what confused you. Yes, future user will need some new features.
    But that does not influence correctness of the middleware core. The
    design of the middleware is modular. New features are added as plug-ins
    and come with the specifications of their own. You can compare that with
    the design of an OS. New OS features are drivers, services, libraries
    etc. None of these normally has any effect on the OS kernel.
    --
    Regards,
    Dmitry A. Kazakov
    http://www.dmitry-kazakov.de

    --- Synchronet 3.19c-Linux NewsLink 1.113
  • From James Harris@james.harris.1@gmail.com to comp.lang.misc on Wed Dec 7 18:13:56 2022
    From Newsgroup: comp.lang.misc

    On 06/12/2022 08:43, Dmitry A. Kazakov wrote:
    On 2022-12-06 00:47, James Harris wrote:

    Rather than a contract (which can lead to an error if the precondition
    is not met) it may be better to /define/ the range of possible inputs
    to a function: a call which has parameters which do not meet the
    definitions will not even match with the function which cannot handle
    them. For example,

       sqrt(-4.0)

    would not match with a definition

       function sqrt(real value :: value >= 0) -> real

    but would match with

       function sqrt(real value) -> complex

    It is untyped. The formal proof is trivial, I gave it already:

       X := 4.0;
       if HALT(p) then
          X := -4.0;
       end if;
       sqrt (X); -- What is the type of?

    What does HALT(p) do?


    The type is undecidable in a legal program, ergo, types are broken.

    The general point is. You cannot map *all* correctness onto types. Types
    are too weak for that. You cannot express *all* behavior in terms of
    types. The example with sqrt perfectly illustrates that.

    --
    James Harris


    --- Synchronet 3.19c-Linux NewsLink 1.113
  • From James Harris@james.harris.1@gmail.com to comp.lang.misc on Wed Dec 7 18:31:53 2022
    From Newsgroup: comp.lang.misc

    On 07/12/2022 16:46, Dmitry A. Kazakov wrote:
    On 2022-12-07 17:37, James Harris wrote:

    Well, formal specifications are more relevant for end-user apps but
    someone writing middleware will not now how the software might be used
    in future.

    As a designer of a middleware I can tell you that for it specifications
    are even more important.

    Specifications for middleware performance? What form do they take?


    It is then important to anticipate potential future uses. As well as
    correctness such software needs to /scale/.

    I see what confused you. Yes, future user will need some new features.
    But that does not influence correctness of the middleware core. The
    design of the middleware is modular. New features are added as plug-ins
    and come with the specifications of their own. You can compare that with
    the design of an OS. New OS features are drivers, services, libraries
    etc. None of these normally has any effect on the OS kernel.

    I don't mean scale in terms of new features but in terms of performance,
    i.e. of execution speed. And possibly in terms of the time taken for individual functions.

    In this context, software which will scale is software of which the run
    time will increase predictably and minimally with higher amounts of data.
    --
    James Harris


    --- Synchronet 3.19c-Linux NewsLink 1.113
  • From Dmitry A. Kazakov@mailbox@dmitry-kazakov.de to comp.lang.misc on Wed Dec 7 20:48:15 2022
    From Newsgroup: comp.lang.misc

    On 2022-12-07 19:31, James Harris wrote:
    On 07/12/2022 16:46, Dmitry A. Kazakov wrote:
    On 2022-12-07 17:37, James Harris wrote:

    Well, formal specifications are more relevant for end-user apps but
    someone writing middleware will not now how the software might be
    used in future.

    As a designer of a middleware I can tell you that for it
    specifications are even more important.

    Specifications for middleware performance? What form do they take?

    Performance is a non-functional requirement, ergo, not a part of formal specifications. Formal specifications contain only functional requirements.

    It is then important to anticipate potential future uses. As well as
    correctness such software needs to /scale/.

    I see what confused you. Yes, future user will need some new features.
    But that does not influence correctness of the middleware core. The
    design of the middleware is modular. New features are added as
    plug-ins and come with the specifications of their own. You can
    compare that with the design of an OS. New OS features are drivers,
    services, libraries etc. None of these normally has any effect on the
    OS kernel.

    I don't mean scale in terms of new features but in terms of performance, i.e. of execution speed. And possibly in terms of the time taken for individual functions.

    I see. In short it is not the business of a component.

    Non-functional requirements like:

    - Security
    - Performance in general
    - Real-time constraints (deadlines etc)
    - Mission critical

    describe a system as whole. You can only say that a component like
    middleware may make fulfilling such requirements easier, more difficult, straightaway impossible. But you cannot ensure them.

    In this context, software which will scale is software of which the run
    time will increase predictably and minimally with higher amounts of data.

    Right. This is why you specify them for the whole system, e.g. a data
    logger that would use middleware, some OS, some hardware etc.
    --
    Regards,
    Dmitry A. Kazakov
    http://www.dmitry-kazakov.de

    --- Synchronet 3.19c-Linux NewsLink 1.113
  • From Dmitry A. Kazakov@mailbox@dmitry-kazakov.de to comp.lang.misc on Wed Dec 7 20:54:57 2022
    From Newsgroup: comp.lang.misc

    On 2022-12-07 19:13, James Harris wrote:
    On 06/12/2022 08:43, Dmitry A. Kazakov wrote:
    On 2022-12-06 00:47, James Harris wrote:

    Rather than a contract (which can lead to an error if the
    precondition is not met) it may be better to /define/ the range of
    possible inputs to a function: a call which has parameters which do
    not meet the definitions will not even match with the function which
    cannot handle them. For example,

       sqrt(-4.0)

    would not match with a definition

       function sqrt(real value :: value >= 0) -> real

    but would match with

       function sqrt(real value) -> complex

    It is untyped. The formal proof is trivial, I gave it already:

        X := 4.0;
        if HALT(p) then
           X := -4.0;
        end if;
        sqrt (X); -- What is the type of?

    What does HALT(p) do?

    Returns True if the program p will halt (e.g. return) or False
    otherwise. But for the sake of argument you can replace it with

    getchar()

    taking user input. The point is that the type of sqrt's result becomes indeterminable.

    Surely you can say, but that is dynamic typing! Right, dynamic typing is
    when you have just one static type. And since there is just one, it is
    always determinable! This is why dynamically typed languages are utterly uninteresting, at least, to me, and to you too, because following that
    path you could simply declare sqrt() complex. Case closed.
    --
    Regards,
    Dmitry A. Kazakov
    http://www.dmitry-kazakov.de

    --- Synchronet 3.19c-Linux NewsLink 1.113
  • From David Brown@david.brown@hesbynett.no to comp.lang.misc on Thu Dec 8 09:28:25 2022
    From Newsgroup: comp.lang.misc

    On 07/12/2022 17:37, James Harris wrote:
    On 06/12/2022 07:56, David Brown wrote:
    On 05/12/2022 23:25, James Harris wrote:
    On 04/12/2022 22:59, David Brown wrote:
    On 04/12/2022 19:21, James Harris wrote:

    ...

    The case in point is two libraries: a memory allocator and a string >>>>> library which uses the memory allocator. What I am saying to you is >>>>> that such libraries need to be correct but they also need to scale
    to large strings, many strings, many string operations, many other
    non-string allocation activity, etc, because it's impossible to say >>>>> just now how they will be used. It's no good to just "focus on
    correctness".


    I did not say that you should ignore performance or efficiency.  I
    said /correctness/ trumps performance every time.  /Every/ time.  No >>>> exceptions.

    You can say it yet again but that doesn't mean it's true. Correctness
    is essential, of course, but for some programs which need hard
    real-time guarantees performance is equally as important.

    What part of "specific timing requirements are part of correctness" is
    giving you trouble here?  Other requirements can be part of
    correctness too - if the customer says the program must be written in
    Quick Basic, then a much faster and neater alternative in Turbo Pascal
    is /wrong/.

    None. I indicated agreement with that statement.

    OK - sorry for misinterpreting you here.



    It really is impossible to stress this too much.  Correctness is
    always key - it doesn't matter how efficient or performant an
    incorrect program is.  If you do not understand that, you are in the
    wrong profession.

    Which part of "Correctness is essential" is giving you trouble? ;-)




    If particular specific performance expectations are part of the
    requirements, then they are part of /correctness/.

    Indeed.


    If you are just looking for "as fast as practically possible given
    the budget constraints for the development process", then that's
    fine too - but it /always/ comes secondary to correctness.

    That is at least a development on what you said before but it is
    still impractical. If you implemented a simple n-squared sort "due to
    budget constraints" it may work fine for weeks but then hang a system
    which got hit with a much larger data set. It does not scale. I dread
    to think how much 'professional' and paid-for software has followed
    similar principles but I've been unfortunate enough to use some of it.


    The problem is not from programmers focusing on correctness.  The main
    problem in software development is failures at the specification level.

    Well, formal specifications are more relevant for end-user apps but
    someone writing middleware will not now how the software might be used
    in future. It is then important to anticipate potential future uses. As
    well as correctness such software needs to /scale/.



    I would say it is the opposite.

    Formal specifications for end-user situations are notoriously difficult.
    How do you define "user-friendly", or "feels comfortable in use" ? Of course it is good to /try/ to specify that kind of thing, but in reality
    you do countless rounds of trials and tests, and in the end the
    specification is "works the way it did on that afternoon when the beta
    testers were happy".

    The deeper into the system you get - through middleware, libraries, and
    down to low-level libraries and language details - the easier it is to
    make a clear specification, and the more important it is to have clear specifications. Make them formal and testable if possible and practical
    (real engineering always has budget constraints!).

    The end user app is specified "show the items in alphabetical order".
    The app will use library functions to do that - and to know which
    library and which functions, these need to be specified in detail
    (scaling, algorithm class, collation method, language support, etc.).

    --- Synchronet 3.19c-Linux NewsLink 1.113
  • From James Harris@james.harris.1@gmail.com to comp.lang.misc on Mon Dec 12 21:07:27 2022
    From Newsgroup: comp.lang.misc

    On 06/12/2022 14:19, David Brown wrote:
    On 05/12/2022 23:38, James Harris wrote:
    On 04/12/2022 23:12, David Brown wrote:
    On 04/12/2022 15:07, James Harris wrote:
    On 04/12/2022 13:27, David Brown wrote:

    ...

    I thought that was what you and Bart (who IIRC also mentioned
    something similar) had in mind and I spend some time designing an
    allocator to support splitting memory in that way down to the byte
    level. It was an interesting exercise and I may use it one day but I
    thought you had some use cases in mind.

    Who cares?  Launch the nasal daemons.

    Never, never, and never!


    Garbage in, garbage out.

    You cannot give the program sensible answers to stupid questions.
    And no matter how much you try to check the program's requests for
    sanity and valid values, there will be a programmer who will outwit
    you and find new ways to be wrong that are beyond your imagination.
    If you have a low-level language that allows things like pointers and
    conversions between points and integers, this will happen /very/
    easily, and there is nothing you can do to stop it.

    ...

    The concept of undefined behaviour - of "garbage in, garbage out" -
    is as old as computing:

    Garbage out is not the same as undefined behaviour. Here's an example
    of garbage in leading to garbage out

       Enter your age: 1000
       You owe us $-1,234,567

    Here's an example of UB

       Enter your age: 1000
       Deleting files in your home directory


    Nasal demons should never be permitted.



    No.

    An example of undefined behaviour is :

    Enter your age between 0 and 200, and I will convert it to Mars years.

    No one has said what will happen here if you enter 1000.  The developer
    can do whatever he or she likes in such cases, including ignoring it entirely, or converting it accurately to Mars years.

    Where did you get that idea of undefined behaviour from? It is different
    and far more benign than anything I've seen before.

    What you describe sounds more like /unspecified/ behaviour to me.
    Undefined is more like the example you gave in another post which ends with

    delete_recursive(path)

    In other words, for undefined behaviour ANYTHING could happen, including something catastrophic. That's why I say it should be prohibited.


    The precondition to the program is simple and clear - it is /your/ fault
    if you put in an age outside the range.

    There is no simple age limit but the behaviour up to a certain limit can
    be specified, along with an undertaking to reject anything higher.


    The designer of a saucepan says what will happen if you put it on a
    normal cooker, or in a dishwasher, or in a home oven - because that is expected use of the saucepan.  He does not say what will happen when you throw it in a blast furnace, or put it on your head and dance naked in
    the street - those are undefined behaviours.  Should those possibilities have been included in the instruction manual for the saucepan?  Should
    the saucepan manufacturer take responsibility for knowing the local laws where you live, and determining if you'll be arrested, locked in a
    padded cell, freeze to death, or simply have a refreshing dance in your
    own garden?

    So why should the implementer of sqrt consider all the ways you could attempt to use the function outside of its specification?

    Mathematical functions can vet their inputs before passing them through
    to the procedure body.
    --
    James Harris


    --- Synchronet 3.19c-Linux NewsLink 1.113
  • From James Harris@james.harris.1@gmail.com to comp.lang.misc on Mon Dec 12 21:13:06 2022
    From Newsgroup: comp.lang.misc

    On 06/12/2022 16:33, Dmitry A. Kazakov wrote:
    On 2022-12-06 14:57, David Brown wrote:

    ...

    I don't think you understand what the term "undefined behaviour"
    means.   Many people don't, even when they are seasoned programmers -
    and that is why they fear it.

    It simply means the specifications don't say anything about the given
    circumstances.

    No, it means exactly what it says: the program's behavior is undefined,
    it can do absolutely anything = be in any possible state. The state of
    the Universe is always confined and variation of states are always
    localized in space and time. Which is why engineering of hardware and programming are so different.

    I agree with your interpretation of undefined behaviour. I hope David
    reviews his position because I recall him saying that he welcomed (or
    similar) UB in a programming language. Definitions matter.
    --
    James Harris


    --- Synchronet 3.19c-Linux NewsLink 1.113
  • From James Harris@james.harris.1@gmail.com to comp.lang.misc on Mon Dec 12 21:17:05 2022
    From Newsgroup: comp.lang.misc

    On 07/12/2022 16:20, James Harris wrote:
    On 06/12/2022 07:46, David Brown wrote:

    ...

    to squeeze in a few extra bytes.  The rest is done by a /new/
    allocation, not an enlargement.  That means all the time spend trying
    to figure out if you have an extra byte or two is wasted.

    Your understanding of the proposal may be improving but there's some way
    to go. The resize call would not be limited to padding for alignment but could return a *lot* more if a lot more were available.

    Maybe it's the small memory requirements I used in the example which is misleading you. They were just illustrative. Say, instead, an existing allocation, D, is 400k and a library using that space wanted to extend
    it by 100k bytes. It would call

      resize(D, 500_000)

    If there was enough space after D for it to be extended to 500k then
    that's what would happen. The size of D would be increased to (at least) 500k without moving the initial 400k to new memory.

    I see no reply to the above. Maybe, David, you now better understand the proposal. Hopefully! :-)
    --
    James Harris


    --- Synchronet 3.19c-Linux NewsLink 1.113
  • From James Harris@james.harris.1@gmail.com to comp.lang.misc on Mon Dec 12 21:46:36 2022
    From Newsgroup: comp.lang.misc

    On 07/12/2022 19:48, Dmitry A. Kazakov wrote:
    On 2022-12-07 19:31, James Harris wrote:
    On 07/12/2022 16:46, Dmitry A. Kazakov wrote:
    On 2022-12-07 17:37, James Harris wrote:

    Well, formal specifications are more relevant for end-user apps but
    someone writing middleware will not now how the software might be
    used in future.

    As a designer of a middleware I can tell you that for it
    specifications are even more important.

    Specifications for middleware performance? What form do they take?

    Performance is a non-functional requirement, ergo, not a part of formal specifications. Formal specifications contain only functional requirements.

    I appear to be somewhere between you and David on this. For sure, it's
    not possible to set user-facing performance figures for software we
    don't know the future uses of! But we can design it so that its run time
    will scale predictably and evenly and reasonably well. That's so for the
    two modules we have been discussing: a string library and a memory
    allocator.
    --
    James Harris


    --- Synchronet 3.19c-Linux NewsLink 1.113
  • From James Harris@james.harris.1@gmail.com to comp.lang.misc on Mon Dec 12 21:50:07 2022
    From Newsgroup: comp.lang.misc

    On 08/12/2022 08:28, David Brown wrote:
    On 07/12/2022 17:37, James Harris wrote:
    On 06/12/2022 07:56, David Brown wrote:
    On 05/12/2022 23:25, James Harris wrote:
    On 04/12/2022 22:59, David Brown wrote:
    On 04/12/2022 19:21, James Harris wrote:

    The case in point is two libraries: a memory allocator and a
    string library which uses the memory allocator. What I am saying
    to you is that such libraries need to be correct but they also
    need to scale to large strings, many strings, many string
    operations, many other non-string allocation activity, etc,
    because it's impossible to say just now how they will be used.
    It's no good to just "focus on correctness".


    I did not say that you should ignore performance or efficiency.  I >>>>> said /correctness/ trumps performance every time.  /Every/ time.
    No exceptions.

    You can say it yet again but that doesn't mean it's true.
    Correctness is essential, of course, but for some programs which
    need hard real-time guarantees performance is equally as important.

    What part of "specific timing requirements are part of correctness"
    is giving you trouble here?  Other requirements can be part of
    correctness too - if the customer says the program must be written in
    Quick Basic, then a much faster and neater alternative in Turbo
    Pascal is /wrong/.

    None. I indicated agreement with that statement.

    OK - sorry for misinterpreting you here.

    There is absolutely nothing to apologise for!!! I appreciate the feedback.

    ...

    Well, formal specifications are more relevant for end-user apps but
    someone writing middleware will not now how the software might be used
    in future. It is then important to anticipate potential future uses.
    As well as correctness such software needs to /scale/.



    I would say it is the opposite.

    Formal specifications for end-user situations are notoriously difficult.
     How do you define "user-friendly", or "feels comfortable in use" ?  Of course it is good to /try/ to specify that kind of thing, but in reality
    you do countless rounds of trials and tests, and in the end the specification is "works the way it did on that afternoon when the beta testers were happy".

    The deeper into the system you get - through middleware, libraries, and
    down to low-level libraries and language details - the easier it is to
    make a clear specification, and the more important it is to have clear specifications.  Make them formal and testable if possible and practical (real engineering always has budget constraints!).

    The end user app is specified "show the items in alphabetical order".
    The app will use library functions to do that - and to know which
    library and which functions, these need to be specified in detail
    (scaling, algorithm class, collation method, language support, etc.).

    It's curious that you have such a view but it's fine that we see it differently. Just talking about performance, I can see a user-facing
    program being written to a precise specification such as "this part must
    sort N personnel records in T seconds" for various N and T. But one
    cannot do that for a general-purpose (non-dedicated) sort library as one
    does not know when writing it how it will be used.

    What one /can/ do is to write it so that its performance scales
    predictably and reasonably evenly and as well as reasonably possible.
    That sounds simple to do but IME it's often overlooked: programmers
    choose algorithms which simply do not scale well. Think of it as
    following the principle of least surprise.

    The point I am making is that when designing a library it's important to
    bear in mind it being used for enormous data sets rather than just for
    small ones.
    --
    James Harris


    --- Synchronet 3.19c-Linux NewsLink 1.113
  • From Dmitry A. Kazakov@mailbox@dmitry-kazakov.de to comp.lang.misc on Tue Dec 13 10:23:56 2022
    From Newsgroup: comp.lang.misc

    On 2022-12-12 22:46, James Harris wrote:
    On 07/12/2022 19:48, Dmitry A. Kazakov wrote:
    On 2022-12-07 19:31, James Harris wrote:
    On 07/12/2022 16:46, Dmitry A. Kazakov wrote:
    On 2022-12-07 17:37, James Harris wrote:

    Well, formal specifications are more relevant for end-user apps but >>>>> someone writing middleware will not now how the software might be
    used in future.

    As a designer of a middleware I can tell you that for it
    specifications are even more important.

    Specifications for middleware performance? What form do they take?

    Performance is a non-functional requirement, ergo, not a part of
    formal specifications. Formal specifications contain only functional
    requirements.

    I appear to be somewhere between you and David on this. For sure, it's
    not possible to set user-facing performance figures for software we
    don't know the future uses of! But we can design it so that its run time will scale predictably and evenly and reasonably well. That's so for the
    two modules we have been discussing: a string library and a memory allocator.

    Yes, in other words, remembering the motto (of rather sad history):

    "don't be evil"

    Programs must be written in good conscience.
    --
    Regards,
    Dmitry A. Kazakov
    http://www.dmitry-kazakov.de

    --- Synchronet 3.19c-Linux NewsLink 1.113
  • From David Brown@david.brown@hesbynett.no to comp.lang.misc on Tue Dec 13 15:16:09 2022
    From Newsgroup: comp.lang.misc

    On 12/12/2022 22:17, James Harris wrote:
    On 07/12/2022 16:20, James Harris wrote:
    On 06/12/2022 07:46, David Brown wrote:

    ...

    to squeeze in a few extra bytes.  The rest is done by a /new/
    allocation, not an enlargement.  That means all the time spend trying
    to figure out if you have an extra byte or two is wasted.

    Your understanding of the proposal may be improving but there's some
    way to go. The resize call would not be limited to padding for
    alignment but could return a *lot* more if a lot more were available.

    Maybe it's the small memory requirements I used in the example which
    is misleading you. They were just illustrative. Say, instead, an
    existing allocation, D, is 400k and a library using that space wanted
    to extend it by 100k bytes. It would call

       resize(D, 500_000)

    If there was enough space after D for it to be extended to 500k then
    that's what would happen. The size of D would be increased to (at
    least) 500k without moving the initial 400k to new memory.

    I see no reply to the above. Maybe, David, you now better understand the proposal. Hopefully! :-)


    I didn't reply because I thought we had covered everything several times already. I understand your proposal - I disagree that it is realistic
    in practice or a good way to implement allocators, strings, or anything
    else. But I am not here to try to force my opinion on you, or convince
    you to change your mind - or to have my mind changed either. We learn
    from discussing different thoughts and opinions, and sharing different experiences, and from putting our own thoughts and ideas into words. I
    expect no more from you than to consider the suggestions or ideas I
    present - implement them, be inspired by them or reject them as it suits.

    So for now, at least, I think we have covered that topic and moved on.

    --- Synchronet 3.19c-Linux NewsLink 1.113
  • From David Brown@david.brown@hesbynett.no to comp.lang.misc on Tue Dec 13 15:38:43 2022
    From Newsgroup: comp.lang.misc

    On 12/12/2022 22:13, James Harris wrote:
    On 06/12/2022 16:33, Dmitry A. Kazakov wrote:
    On 2022-12-06 14:57, David Brown wrote:

    ...

    I don't think you understand what the term "undefined behaviour"
    means.   Many people don't, even when they are seasoned programmers - >>> and that is why they fear it.

    It simply means the specifications don't say anything about the given
    circumstances.

    No, it means exactly what it says: the program's behavior is
    undefined, it can do absolutely anything = be in any possible state.
    The state of the Universe is always confined and variation of states
    are always localized in space and time. Which is why engineering of
    hardware and programming are so different.

    I agree with your interpretation of undefined behaviour. I hope David reviews his position because I recall him saying that he welcomed (or similar) UB in a programming language. Definitions matter.



    I do welcome UB in programming languages - I think it is a good thing to
    be clear and up-front about it. It is a bad idea for a language to try
    and hide its limitations and give the impression that it will always
    save the programmer from their mistakes. Programmers are already too
    lazy, even in languages with UB ("I don't need to check if this alloc
    returns a null pointer - the OS will handle the crash neatly if I'm wrong").

    I much prefer a clear separation of responsibilities and guarantees.
    The programmer promises to write code that follows certain rules, and
    the language/library promises to give certain effects as long as the
    rules are followed. As a language user, I don't want a language or
    library that has extra inefficiencies, complications or irregularities
    to support programmers who don't learn the language and follow its
    rules. As a language implementer or library implementer, I don't want
    to have to try to figure out what the user meant or conjure up sensible answers to senseless questions.

    Trust the programmer. Trust the language and the libraries. Everyone
    is happier that way - it's all simpler, cleaner, and more efficient.

    And then provide tools that can help people find their mistakes and
    improve their code!


    Yes, definitions matter. So do lack of definitions. Undefined
    behaviour has /no/ definition - by the definition of the words
    themselves! Anyone who thinks they have an "interpretation of undefined behaviour" has missed the point entirely - it has no definition or interpretation.


    Any function (or operator, or other feature of the language or library)
    should be specified in terms of two things:

    1. Precondition.
    2. Postcondition.

    The precondition says what must be true before the function is called.
    It includes the number and types of the parameters, any limitations on
    the parameters, any global data or environmental conditions, etc. The programmer promises that this expression will be "true" when the
    function is called. The function implementer can, and should, rely upon
    it being true - any effort spent handling situations when it is not true
    is wasted effort and inefficiency.

    The postcondition says what is true when the function returns, and
    includes the number and types of the return values, any effects on
    global data or the environment, side-effects, etc. The function
    implementer's job is to ensure the postcondition is true on the
    assumption that the precondition is true. The function user can then
    rely upon it being true after the call - any effort spend checking it is wasted effort.


    "Undefined behaviour" is then simply calling the function when the precondition is not true. What happens if you try? The function specification does not say. End of story.
    --- Synchronet 3.19c-Linux NewsLink 1.113
  • From David Brown@david.brown@hesbynett.no to comp.lang.misc on Tue Dec 13 16:17:49 2022
    From Newsgroup: comp.lang.misc

    On 12/12/2022 22:07, James Harris wrote:
    On 06/12/2022 14:19, David Brown wrote:
    On 05/12/2022 23:38, James Harris wrote:
    On 04/12/2022 23:12, David Brown wrote:
    On 04/12/2022 15:07, James Harris wrote:
    On 04/12/2022 13:27, David Brown wrote:

    ...

    I thought that was what you and Bart (who IIRC also mentioned
    something similar) had in mind and I spend some time designing an
    allocator to support splitting memory in that way down to the byte
    level. It was an interesting exercise and I may use it one day but
    I thought you had some use cases in mind.

    Who cares?  Launch the nasal daemons.

    Never, never, and never!


    Garbage in, garbage out.

    You cannot give the program sensible answers to stupid questions.
    And no matter how much you try to check the program's requests for
    sanity and valid values, there will be a programmer who will outwit
    you and find new ways to be wrong that are beyond your imagination.
    If you have a low-level language that allows things like pointers
    and conversions between points and integers, this will happen /very/
    easily, and there is nothing you can do to stop it.

    ...

    The concept of undefined behaviour - of "garbage in, garbage out" -
    is as old as computing:

    Garbage out is not the same as undefined behaviour. Here's an example
    of garbage in leading to garbage out

       Enter your age: 1000
       You owe us $-1,234,567

    Here's an example of UB

       Enter your age: 1000
       Deleting files in your home directory


    Nasal demons should never be permitted.



    No.

    An example of undefined behaviour is :

    Enter your age between 0 and 200, and I will convert it to Mars years.

    No one has said what will happen here if you enter 1000.  The
    developer can do whatever he or she likes in such cases, including
    ignoring it entirely, or converting it accurately to Mars years.

    Where did you get that idea of undefined behaviour from? It is different
    and far more benign than anything I've seen before.

    It's not benign or malign - it is undefined. (I could have added "the
    program signs you up for a one-way ticket to Mars" as a possible result.)


    What you describe sounds more like /unspecified/ behaviour to me.
    Undefined is more like the example you gave in another post which ends with

      delete_recursive(path)

    In other words, for undefined behaviour ANYTHING could happen, including something catastrophic. That's why I say it should be prohibited.


    Undefined behaviour means nobody has defined what will happen.

    The /possibility/ of undefined behaviour in a function is a good thing.
    It is very rare that it makes sense to define the behaviour for
    absolutely any input in absolutely any situation. And no matter how
    hard you try, you can't cover /everything/ - some things are outside
    your control. How is your function supposed to behave when a cosmic ray causes an SEU in the processor leading to an unexpected jump? What is
    the result of your square root function when the user spills coffee on
    the computer? How will it react to a stack overflow?

    Trying to /execute/ undefined behaviour is the problem.

    int add_and_divide(int a, int b, int c) {
    return (a + b) / c;
    }

    This has undefined behaviour in C - it is UB if c is 0, or if (a + b) overflows, or if (a + b) equals INT_MIN and c equals -1. (I bet you
    didn't even think of that last one.)

    It is absolutely /fine/, as long as its limitations are clear.

    There is only a problem if the code calling "add_and_divide()" has
    errors, and calls it with inappropriate arguments. So yes, prohibit it
    - tell the programmer not to do that, because it would be a bug in the
    code. (It may be that the fault lies with the documenter of the
    function, or the person specifying the code, or the programmer that
    calls the function that calls add_and_divide, but it is still a bug.)

    All run-time undefined behaviour that is executed is the result of buggy software. Further, all bugs result in undefined behaviour - by
    definition, a "bug" means the code is not doing what it is supposed to
    do, and thus the program is not behaving according to the defined specification.)


    The worst thing you can do - IMHO, as always - is pretend that you can
    "solve" the "problem" of undefined behaviour by trying to define the
    behaviour of everything. You want to make a language were it is
    impossible for the programmer to have bugs in their code? Good luck
    with that! The "best" you can do is tell programmers you think they are
    so incompetent that you'd rather slow down their code and give them meaningless and wrong results than trust them to do their job properly.


    (Again, let me stress it is useful for tools - compiler warnings,
    linters, analysers, IDE's, debuggers, run-time sanitizers, etc., - to
    help find and correct bugs at all levels.)




    The precondition to the program is simple and clear - it is /your/
    fault if you put in an age outside the range.

    There is no simple age limit but the behaviour up to a certain limit can
    be specified, along with an undertaking to reject anything higher.


    That would be changing the precondition and the postcondition. Your
    program no longer has a limit to the value you put in. But it is no
    longer a program that calculates the sum owed, or your age in Mars
    years. It is now a program that /sometimes/ does that, and sometimes
    does something else, depending on what value you enter. So it is a significantly different specification for a significantly different
    program. Maybe that's fine, maybe not.

    (When you have unsanitized data coming in from the outside, it is almost invariably best to have a specification that includes checks and limits
    on the data.)



    The designer of a saucepan says what will happen if you put it on a
    normal cooker, or in a dishwasher, or in a home oven - because that is
    expected use of the saucepan.  He does not say what will happen when
    you throw it in a blast furnace, or put it on your head and dance
    naked in the street - those are undefined behaviours.  Should those
    possibilities have been included in the instruction manual for the
    saucepan?  Should the saucepan manufacturer take responsibility for
    knowing the local laws where you live, and determining if you'll be
    arrested, locked in a padded cell, freeze to death, or simply have a
    refreshing dance in your own garden?

    So why should the implementer of sqrt consider all the ways you could
    attempt to use the function outside of its specification?

    Mathematical functions can vet their inputs before passing them through
    to the procedure body.


    Mathematical functions - /real/ mathematical functions - are often
    defined on limited domains. The mathematical square root function on
    reals is not defined for negative values. √-1 is undefined behaviour. Trying to define the function as mapping from ℝ to ℝ ∪ { "Error: Negative square root" } would be an utter disaster in mathematics.

    At its heart, programming is a branch of mathematics. Trying to define
    the indefinable can appear tempting, but it is a bad idea in the long run.

    --- Synchronet 3.19c-Linux NewsLink 1.113
  • From Bart@bc@freeuk.com to comp.lang.misc on Tue Dec 13 15:26:24 2022
    From Newsgroup: comp.lang.misc

    On 13/12/2022 14:38, David Brown wrote:
    On 12/12/2022 22:13, James Harris wrote:

    I agree with your interpretation of undefined behaviour. I hope David
    reviews his position because I recall him saying that he welcomed (or
    similar) UB in a programming language. Definitions matter.



    I do welcome UB in programming languages - I think it is a good thing to
    be clear and up-front about it.

    Except that, with C, and exploitative C compilers, that doesn't happen.
    They simply assume UB can never happen, and use that as an excuse to be
    able to do what they like, and you didn't expect. That's the opposite of
    being up-front!


    --- Synchronet 3.19c-Linux NewsLink 1.113
  • From David Brown@david.brown@hesbynett.no to comp.lang.misc on Tue Dec 13 18:12:57 2022
    From Newsgroup: comp.lang.misc

    On 13/12/2022 16:26, Bart wrote:
    On 13/12/2022 14:38, David Brown wrote:
    On 12/12/2022 22:13, James Harris wrote:

    I agree with your interpretation of undefined behaviour. I hope David
    reviews his position because I recall him saying that he welcomed (or
    similar) UB in a programming language. Definitions matter.



    I do welcome UB in programming languages - I think it is a good thing
    to be clear and up-front about it.

    Except that, with C, and exploitative C compilers, that doesn't happen.
    They simply assume UB can never happen, and use that as an excuse to be
    able to do what they like, and you didn't expect. That's the opposite of being up-front!


    No, they are up-front about it. The language standards say, for
    example, that signed integer overflow is undefined behaviour. You don't
    have any justification for any expectations about what might happen if
    you try to overflow your integers, or dereference a null pointer.

    It's really not difficult in most cases. If you have 16-bit integers (I always use that in Usenet posts, because the numbers are easier!), then
    it is quite clear that you cannot add 20,000 and 15,000 and expect a mathematically correct answer. I have no expectations for what would
    happen if I tried to do something that is clearly wrong mathematically,
    and clearly wrong in the definition of the language. The language says
    it has no meaning, the compiler says it implements the language - what
    more could I ask for?



    --- Synchronet 3.19c-Linux NewsLink 1.113