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!
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!
On 25/11/2022 14:02, James Harris wrote:
It can make sense to distinguish between "alloc_zeroed" and "alloc_unintialised", for performance reasons.
I think you should not try to copy C's malloc/free mechanism.
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.
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.
On 25/11/2022 13:02, James Harris wrote:It's an interesting topic. I can think of three really fast memory allocation approaches:
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.
Unfortunately, none of those work well for the dynamic creation and destruction of strings - which is what I need just now.
On 25/11/2022 14:02, James Harris wrote: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.
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_allocIn a new language, I would not follow the names from C as closely. For
m_calloc
m_realloc
m_resize
m_free
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.Agreed, but what exactly would you want for the calls? Maybe
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.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.
Bart said the same and the suggestion surprises me. It seems prone to error.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,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.
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 differentI 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.
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 andAgreed.
care, especially for thread safety. But the emphasis must be on cache friendliness.)
On 2022-11-25 17:15, David Brown wrote: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.
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.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.
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?
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?
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:alloc_unmapped/unpaged
It can make sense to distinguish between "alloc_zeroed" and
"alloc_unintialised", for performance reasons.
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.
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.
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.
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!
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;;
}
}
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.
I think you should not try to copy C's malloc/free mechanism.
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.
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.
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 byteThe code that tracks capacity is vastly cleverer than code that
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.
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.
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 improveIn a new language, I would not follow the names from C as closely.
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
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)
?
No. It is pointless.
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).
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 ofI think you should not try to copy C's malloc/free mechanism. In
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!
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.
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 aThe code that tracks capacity is vastly cleverer than code that
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.
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.
I cannot see any point in first asking how much space I /really/ have, before asking for the 1020 bytes. What is the point?
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?
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.
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.
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.
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,
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.
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.
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.
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.
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.
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.
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.)
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.
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.
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).
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.
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.
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.
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
(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.
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.
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.
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.
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!
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.
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.
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.
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.
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.
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.
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!
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?
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.
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.
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.
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.
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?
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.
Again, it is NOT a memory pool allocator. Do not use the toilet cleaner
to brush your teeth!
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.
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?
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.
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.
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.
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/.
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.
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.
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.
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.
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.
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.
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.
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?
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.
ore about code /correctness/ - avoiding bugs,
or at least making it easier to find bugs
inefficiencies that do not affect code usability.
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?
If all software was perfect, every program would be on version 1.000.
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.
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).
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/.
You need to acknowledge when there is a real program, not just blindly
throw more resources at it.
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.
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.
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 hasAgreed. I throw an exception.
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.
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.
On 29/11/2022 16:54, David Brown wrote:
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
?
Agreed. I throw an exception.
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.
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.
Well, the software would be in layers.
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.)
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.
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.
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)
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.
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/.
Great example. I was heartened recently to hear people makingIf your code is not correct, it doesn't matter how quickly you get the
performance a key design goal. Performance can be just as much a part of engineering a solution as correctness.
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.
On 03/12/2022 20:34, James Harris wrote:
Performance can be just as much a part
of engineering a solution as correctness.
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.
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?
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:
...
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 noThe 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/. >>>>>
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.
Well, the software would be in layers.
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.)
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. :-)
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?
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".
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)
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!
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.
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.
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.
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
"""
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
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!)
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.
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).
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.
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.
A weak precondition makes the function more flexible to call, but harder
to implement.
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,
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!
The second version of the specification is clear and simple.
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.
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...
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.
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".
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.
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?
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.
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.
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?
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.
It is /better/ to leave situations that must not be allowed to occur, as undefined.
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?
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.
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.
But your version doesn't have undefined behaviour - throwing an
exception is part of the specification.
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.
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.
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?
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.
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.
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.
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.
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"
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.
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:
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.
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)
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!
Remember the old joke about politicians in a crisis? "We must do something! This is something - therefore we must do it!".
On 2022-12-05 17:56, David Brown wrote:
Any defined behavior is better than any undefined one.
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?
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.
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.
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!
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
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.
(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.
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.
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.
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?
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.
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 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".
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,
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.
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.
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!
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?
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 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!
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!
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.
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);
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.
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.
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.
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.
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.
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.
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.
I doubt if we are ever going to agree here. Maybe it's time to move on?
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.
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.)
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.
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.
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.
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.
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/.
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.
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.
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.
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?
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/.
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?
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.
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.
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.
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.
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.).
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.
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! :-)
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.
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.
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.
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!
Sysop: | DaiTengu |
---|---|
Location: | Appleton, WI |
Users: | 764 |
Nodes: | 10 (2 / 8) |
Uptime: | 253:06:23 |
Calls: | 10,560 |
Calls today: | 2 |
Files: | 185,887 |
Messages: | 1,673,469 |