Groups | Search | Server Info | Keyboard shortcuts | Login | Register [http] [https] [nntp] [nntps]
Groups > comp.lang.c > #167919 > unrolled thread
| Started by | BGB <cr88192@gmail.com> |
|---|---|
| First post | 2022-09-30 16:34 -0500 |
| Last post | 2022-10-03 09:29 +0000 |
| Articles | 14 — 5 participants |
Back to article view | Back to comp.lang.c
Idle: C library features wish-list. BGB <cr88192@gmail.com> - 2022-09-30 16:34 -0500
Re: Idle: C library features wish-list. Tim Rentsch <tr.17687@z991.linuxsc.com> - 2022-10-02 20:38 -0700
Re: Idle: C library features wish-list. BGB <cr88192@gmail.com> - 2022-10-03 01:14 -0500
Re: Idle: C library features wish-list. Kaz Kylheku <864-117-4973@kylheku.com> - 2022-10-03 20:40 +0000
Re: Idle: C library features wish-list. scott@slp53.sl.home (Scott Lurndal) - 2022-10-03 21:10 +0000
Re: Idle: C library features wish-list. BGB <cr88192@gmail.com> - 2022-10-03 19:07 -0500
Re: Idle: C library features wish-list. scott@slp53.sl.home (Scott Lurndal) - 2022-10-04 13:43 +0000
Re: Idle: C library features wish-list. BGB <cr88192@gmail.com> - 2022-10-04 15:20 -0500
Re: Idle: C library features wish-list. BGB <cr88192@gmail.com> - 2022-10-05 03:58 -0500
Re: Idle: C library features wish-list. Kaz Kylheku <864-117-4973@kylheku.com> - 2022-10-04 16:56 +0000
Re: Idle: C library features wish-list. BGB <cr88192@gmail.com> - 2022-10-04 18:30 -0500
Re: Idle: C library features wish-list. BGB <cr88192@gmail.com> - 2022-10-03 18:19 -0500
Re: Idle: C library features wish-list. Tim Rentsch <tr.17687@z991.linuxsc.com> - 2022-11-19 06:54 -0800
Re: Idle: C library features wish-list. gazelle@shell.xmission.com (Kenny McCormack) - 2022-10-03 09:29 +0000
| From | BGB <cr88192@gmail.com> |
|---|---|
| Date | 2022-09-30 16:34 -0500 |
| Subject | Idle: C library features wish-list. |
| Message-ID | <th7ne1$140s1$2@dont-email.me> |
There are some things that come up often that it might be "useful" if
they could be supported in a more portable ways.
Some of these exists as extensions in various libraries (and some of
this is specific to my own project, which may also happen to include its
own C compiler and runtime).
Maybe add disclaimers that none of this is intended for general
portability outside of a specific (albeit very common) class of target
architectures.
Actual function names subject to vary, but "general idea" mostly.
I don't expect any of this to amount to much.
Getting/setting values of defined size and endianess given a pointer:
uint32_t _mget_uint32le(void *ptr);
void _mset_uint32le(void *ptr, uint32_t val);
...
uint64_t _mget_uint64be(void *ptr);
void _mset_uint64be(void *ptr, uint64_t val);
...
Say, the pointer is assumed to be unaligned, endianess is explicit. It
is the responsibility of the compiler or runtime to make it work in
whatever way is most efficient for that target.
Malloc/heap related:
size_t _msize(void *ptr);
Get the size of the pointed to heap object.
Returns 0 if pointer is not valid a valid heap object.
Need not match (exactly) the size given to malloc.
void *_mgetbase(void *ptr);
Get the base-address of a previously malloc'ed pointer.
Returns NULL if the pointer does not point at a malloc'ed object.
void *_malloc_cat(size_t size, int mode);
Allocate an object which is usable according to the mode flags.
MALLOC_MCAT_RW //Read/Write, Default
MALLOC_MCAT_RWX //Read/Write/Execute
MALLOC_MCAT_ZTAG //Object needs to support tags and zone.
...
(Also partially overlaps with mmap's prot/map flags)
_mgetbase would return the pointer originally returned by malloc or
_malloc_cat or similar, if given a pointer into an object on the heap.
If given the base pointer for a malloc'ed object, it will return this
pointer.
The _malloc_cat call allocates by "category", and this category may not
be changed after the fact (since the categories may be in different
parts of the address space or similar; say because they involve
different mmap's).
Something like "realloc()" would preserve the relevant parts of the
category when copying an object.
By default, memory returned by malloc and similar would be assumed to be
non-executable, with the ability to allocate executable memory being a
special case. Specifics of "actually using" using RWX memory will depend
on the target (usually assumes writing target-specific machine code into
a buffer and some means of flushing the instruction cache and similar).
The _msize call would not be expected to preserve the exact size from
the original malloc call:
Don't necessarily want to store the exact size, but an approximate size
will be known;
It typically makes sense to "quantize" or "bucket" the allocation sizes,
so the stored object will typically be larger than the size it was
originally allocated as (both due to alignment padding and also because
this can help reduce heap fragmentation).
Say, for example, one malloc's 1392 bytes and the underlying object can
hold 1536 or similar, etc.
But, maybe add to this:
uint16_t _mgettag(void *ptr);
void _msettag(void *ptr, uint16_t ttag);
These get/set a user-defined tag value.
This would be something like an object type-tag or similar.
Default tag is 0.
Tag may always return as 0 if object does not support tags.
Zone, possible:
uint16_t _mgetzone(void *ptr);
void _msetzone(void *ptr, uint16_t ztag);
Get/Set a 'zone' tag.
void _mfreezone(uint16_t ztag, uint16_t zmask);
Free all objects on the heap where ((obj->ztag&zmask)==ztag).
Trying to free (ztag==0) is undefined (may abort/crash).
Default zone tag is 0.
Tag may always return as 0 if object does not support zone tags.
These do imply that the malloc implementation have a per-object header
with enough information needed to encode the size and relevant tags for
the object. For small objects, this may be undesirable.
One may need to request support explicitly. For objects allocated with
bare malloc, it would be undefined whether or not these tags exist.
The zone system may be itself used either as a crude garbage collector,
or used to implement the "sweep" stage of a garbage collector.
However, the assumption here is that the implementation does not itself
necessarily provide a garbage collector (at least, not enabled by
default, and will not apply to normal use of malloc/free).
An implementation need not have these "actually work", so the minimum
"required" behavior would be that the tag functions return 0, and
_mfreezone() does nothing.
Otherwise, "_mfreezone()" is potentially very slow, and is assumed to be
used infrequently (since it does basically walk the whole heap and
bulk-frees anything which matches the requested tag pattern).
While one could argue for other things per-object, like a
reference-count or "color" bits, one could also potentially make a case
for having the program itself use the zone-tag bits for doing this, say:
(15:8): Zone Level or Zone-Node
( 7:2): Reference Count
( 1:0): Object Color
As for 16-bit tags (vs 32-bit):
The 16-bit tags should mostly be sufficient for most of these use-cases,
whereas 8 bits may not be sufficient;
Support for these tags would need to be paid for by nearly every object
on the heap, so cost is a concern.
Yeah, granted, these ones are pretty non-standard.
Trying to avoid getting too much into the specifics of how this stuff
would be implemented.
There are some features which can be useful, such as the ability to
register finalizer callbacks on top of the zone system:
int _mzone_add_finalizer(
uint16_t ttag, uint16_t tmask,
uint16_t ztag, uint16_t zmask,
void (*func)(void *ptr));
Calls func whenever _mfreezone frees an object and:
(((obj->ttag&tmask)==ttag) && ((obj->ztag&zmask)==ztag))
Note that the implementation "may" combine these fields internally into
a single 32-bit field or similar. Here, "_mfreezone" will call
finalizers, but "free()" will not do so (and if the program wants to
NULL out user-pointers or similar, it may do so via the finalizer callback).
Note that a finalizer will only necessarily be called for an object if
the finalizer was registered prior to the object's tags being set
(otherwise, undefined). Which finalizer is called if multiple finalizers
can match a given bit-mask pattern is also undefined (though, could be
evaluated separately for both ztag and ttag, in which case potentially
two finalizers could be called for the same object).
If the implementation provides its own tagged type-system and GC
facilities, then it is also possible that the implementation uses some
range of the tag spaces for itself, say:
0000..3FFF: Reserved for runtime.
4000..7FFF: Available for application use.
8000..FFFF: TBD
Note that the finalizer should not be seen as equivalent to an object
destructor. Both may exist, but would be handled in different ways and
by different mechanisms.
While this also does not directly provide for "this object being freed
also sets other pointers to this object to NULL" semantics, this
functionality can be faked by creating a finalizer which sets the
pointers to NULL (and otherwise, this sort of thing is obscure enough in
practice to not make it worth wasting space in the object headers for
sake of supporting this).
One other optional requirement is that pointers returned by "malloc" and
friends are able to be used (and freed), from any library in the running
program instance, and across thread boundaries, etc. This sort of thing
is an annoyance in MSVC, I would prefer if "malloc from anywhere and
free from anywhere else" be required de-facto (without extra hassle).
Well, by extension would prefer if stdio and "FILE *" also did this,
stuff like passing an open FILE* descriptor from one DLL to another, and
then trying to perform IO on it from that other DLL, and having the C
library promptly grenade itself, is pointless and annoying.
Usual workaround is to provide a wrapper interface to redirect all of
the IO back to a common location (along with of "extra stuff" if one
wants using "stdio.h" stuff to also be thread safe); would prefer it if
this stuff working could be assumed to work as a default.
Granted, yes, Linux and friends already have the behavior I am looking
for here (mostly would just prefer it more if this were universal).
Copy/compare:
void _memlzcpy(void *dst, void *src, size_t sz);
Copy the buffer as-if it were implemented as:
unsigned char *cs, *ct, *cse;
ct=dst; cs=src; cse=cs+sz;
while(cs<cse)
*ct++=*cs++;
However, unlike the naive loop, hopefully in a way that is not so
painfully slow:
Non-overlap: Behaves like memcpy;
Backwards (dst<src): Behaves like memmove;
Forwards overlap (dst>src): Fill with a repeating byte pattern.
This pattern basically forms the core of an LZ77 decompressor, and the C
library could potentially do a "better" job at this than asking every LZ
decompressor to supply their own implementation (which then usually
needs to at least pay lip service to portability, and will often still
be slower than ideal for a given target machine, such as by using 32 or
64 bit element copies on a machine which allows copying the data 128
bits at a time or similar).
void _memlzcpyf(void *dst, void *src, size_t sz);
Similar to the former, but "fast".
Potentially, unlike the former, it does not assume the need to copy an
"exact" number of bytes, but could be allowed to "stomp" some number of
bytes past the end of the destination (needing to fall back to byte-copy
for the last few bytes or so can be a significant time-waster in these
use cases). This "slack area" would depend on the implementation (say,
16, 32, or 64 bytes).
size_t _memlzcmp(void *srca, void *srcb, size_t szlim);
Perform an LZ style compare, returning the number of bytes which are an
exact match up to the size limit.
This is a similar sort of situation, where the matching stage in a
compressor needs to do this a lot, and the usual options are either:
Big convoluted mess (most of the time);
Naive byte-for-byte compare, usually unacceptably slow.
Usually there is also a bunch of hashing and hash-chain walking or
similar, but this part can be left to the specific encoder (along with
everything else).
Etc...
There is probably other stuff, but these mostly represent a few common
pain points that tend to pop up fairly often.
Any thoughts?...
[toc] | [next] | [standalone]
| From | Tim Rentsch <tr.17687@z991.linuxsc.com> |
|---|---|
| Date | 2022-10-02 20:38 -0700 |
| Message-ID | <86y1txinka.fsf@linuxsc.com> |
| In reply to | #167919 |
BGB <cr88192@gmail.com> writes: > There are some things that come up often that it might be "useful" if > they could be supported in a more portable ways. > > [ ... ] > > Any thoughts?... None of these is suitable for inclusion in the ISO C standard.
[toc] | [prev] | [next] | [standalone]
| From | BGB <cr88192@gmail.com> |
|---|---|
| Date | 2022-10-03 01:14 -0500 |
| Message-ID | <thdum3$23pod$2@dont-email.me> |
| In reply to | #167946 |
On 10/2/2022 10:38 PM, Tim Rentsch wrote: > BGB <cr88192@gmail.com> writes: > >> There are some things that come up often that it might be "useful" if >> they could be supported in a more portable ways. >> >> [ ... ] >> >> Any thoughts?... > > None of these is suitable for inclusion in the ISO C standard. Possibly. As noted, this was more an "idle wish list", based mostly on stuff that comes up a lot in my experience, not really a proposal that this stuff be added (as-is) to the C standard. A few of them, such as _msize(), exist in MSVCRT, and is functionally equivalent to malloc_usable_size() in GLIBC. Also was using the "_whatever()" naming convention, because this seems to be the typical naming convention for library extensions (vs "__whatever" being more typical for compiler-specific keywords). This is stuff that comes up a lot, and almost every non-trivial program needs to implement a lot of this itself (often multiple times, if one uses libraries and every library provides its own implementation). Like, say, for example, what if the C library had not provided "memcpy()" and similar, and nearly every application was left to roll their own, often doing so poorly. Also, C11 threads exist, so one never really knows what sorts of stuff they might throw in... Most of this does exist in the C library used in by my compiler (itself a highly modified fork of PDPCLIB, *1), but as noted, this does limit its scope (since for code portability one is limited mostly to the least common denominator between the targets in question). Well, there are also a bunch of C language extensions, but I decided to leave these out in this case (same sort of issue), and some amount exist partly as quirky side effect of trying to squeeze performance out of a 50 MHz CPU running on top of an FPGA (mostly on an Artix-7 based board). Well, among other funkiness. Well, and also C isn't the only language I am running here (and the compiler and runtime exist with some stuff partly intended for use by the other languages I am compiling with my compiler). *1: I have ended up rewriting a fair chunk of the C library as some of the code was "kinda awful" (but, it is more of a "ship of Theseus" thing). Had a few times considered possibly doing a full rewrite, mostly motivated by architectural reasons, but had not fully done so yet partly due to inertia. ..
[toc] | [prev] | [next] | [standalone]
| From | Kaz Kylheku <864-117-4973@kylheku.com> |
|---|---|
| Date | 2022-10-03 20:40 +0000 |
| Message-ID | <20221003130059.385@kylheku.com> |
| In reply to | #167947 |
On 2022-10-03, BGB <cr88192@gmail.com> wrote:
> On 10/2/2022 10:38 PM, Tim Rentsch wrote:
>> BGB <cr88192@gmail.com> writes:
>>
>>> There are some things that come up often that it might be "useful" if
>>> they could be supported in a more portable ways.
>>>
>>> [ ... ]
>>>
>>> Any thoughts?...
>>
>> None of these is suitable for inclusion in the ISO C standard.
>
> Possibly.
>
>
> As noted, this was more an "idle wish list", based mostly on stuff that
> comes up a lot in my experience, not really a proposal that this stuff
> be added (as-is) to the C standard.
I don't know why you would even wish to have most of that stuff in the
standard.
The standard would be objectively worse, even for you, whenever
you're working on anything but the one program where you need any
of it.
> A few of them, such as _msize(), exist in MSVCRT, and is functionally
> equivalent to malloc_usable_size() in GLIBC.
_msize doesn't return the size that was passed to malloc; it returns
some rounded up size. Still that can be useful.
Code which manages a buffer that grows when it becomes full
tracks the allocated size from the actual filled size. With this
function, you don't have to waste space storing the allocated
size and keeping it up-to-date: you just retrieve it. Moreover,
you use the full underlying size without any waste.
The function would have to be specified such that if you malloc(42),
and then malsize(ptr) yields 64, it becomes legitimate for you
to make use of all the bytes bytes 0 to 63.
Moreover, it would have to be specified that malsize(ptr) is called, and
returns some value, then it must always return a value at least as large
for ptr, regardless of any memory allocations or deallocations that take
place.. The memory indicated by that size must really belong to the
allocated object.
> Like, say, for example, what if the C library had not provided
> "memcpy()" and similar, and nearly every application was left to roll
> their own, often doing so poorly.
Sure, but how many need a memcpy that allows overlap, but if the
overlap is in the wrong direction, it then repeats a byte?
A memcpy that allows overlap, if the second operand has a higher
address than the first, would be mildly useful. However,
if we say that the second address must be higher, that can be
satisfied by it being higher only by a byte.
The motivation for that function is that a simple loop can perform the
copy, which sweeps over both operands in order of increasing address.
However, it can only work reliably if the transfer unit's width
is no larger than the displacement between the two buffers.
So in the case of a one byte difference, the loop must transfer
a byte at a time.
In cases when the address delta can't be deduced at compile time,
that function would have to switch on the delta size, and say
handle the 1, 2, 4 and 8 byte cases specially. Plus handle the
alignment cases and all that.
It's not clear that it would end up winning very much over memmove.
Programmers who want the most performance out of memcpy just
make it non-overlapping.
Versions of memcpy and memmove which allow the application to
specify the alignment (whereby the application ensures that
the promised alignemnt is true) would be useful:
/* array copy, array move */
/* non-overlapping operands.
both operand pointers aligned to elem_size, else UB. */
arrcpy(dest, src, elem_cnt, elem_size)
/* Possibly overlapping operands.
both operand pointers aligned to elem_size, else UB. */
arrmove(dest, src, elem_cnt, elem_size)
Copy operations that don't have to handle run-time alignment cases, and
odd leftover sizes, could likely be implemented faster.
The elem_size expression is often a constant expression, in which cases
the compiler can rewrite the call to use a function which handles that
transfer unit size (or multiples), without worrying about alignment or
partial transfer units at the end.
ISO C (since 99) has something like this, for wchar_t: wmemcpy
and wmemmove. The above functions would just generalize that.
--
TXR Programming Language: http://nongnu.org/txr
Cygnal: Cygwin Native Application Library: http://kylheku.com/cygnal
[toc] | [prev] | [next] | [standalone]
| From | scott@slp53.sl.home (Scott Lurndal) |
|---|---|
| Date | 2022-10-03 21:10 +0000 |
| Message-ID | <OcI_K.166108$w35c.86764@fx47.iad> |
| In reply to | #167953 |
Kaz Kylheku <864-117-4973@kylheku.com> writes: >On 2022-10-03, BGB <cr88192@gmail.com> wrote: >> Like, say, for example, what if the C library had not provided >> "memcpy()" and similar, and nearly every application was left to roll >> their own, often doing so poorly. > >Sure, but how many need a memcpy that allows overlap, but if the >overlap is in the wrong direction, it then repeats a byte? > >A memcpy that allows overlap, if the second operand has a higher >address than the first, would be mildly useful. However, >if we say that the second address must be higher, that can be >satisfied by it being higher only by a byte. Why would someone want to use memcpy for an overlapping move when memmove is available to handle the overlap cases? If one programs in C, one should RTFM, and the FM says "The memory areas must not overlap" As an aside, the latest extensions to the ARMv8 architecture include instructions to implement memset and memcpy in an hardware efficient manner. DAGS DDI0487I_a_a-profile_architecture_reference_manual.pdf and look at the 30 CPY* instructions (and they still call it RISC :-), or the SETP/SETM/SETE instructions. The key is that the instructions allow the processor to move data in the most effective unit-size (e.g. a cache line) rather than byte-at-a-time (or the C library mem* assembler functions using 64-bit or 128-bit accesses in a loop). It's still a loop, but the processor determines how much is moved in each iteration rather than the programmer.
[toc] | [prev] | [next] | [standalone]
| From | BGB <cr88192@gmail.com> |
|---|---|
| Date | 2022-10-03 19:07 -0500 |
| Message-ID | <thfthv$2bf07$1@dont-email.me> |
| In reply to | #167954 |
On 10/3/2022 4:10 PM, Scott Lurndal wrote:
> Kaz Kylheku <864-117-4973@kylheku.com> writes:
>> On 2022-10-03, BGB <cr88192@gmail.com> wrote:
>
>>> Like, say, for example, what if the C library had not provided
>>> "memcpy()" and similar, and nearly every application was left to roll
>>> their own, often doing so poorly.
>>
>> Sure, but how many need a memcpy that allows overlap, but if the
>> overlap is in the wrong direction, it then repeats a byte?
>>
>> A memcpy that allows overlap, if the second operand has a higher
>> address than the first, would be mildly useful. However,
>> if we say that the second address must be higher, that can be
>> satisfied by it being higher only by a byte.
>
> Why would someone want to use memcpy for an overlapping
> move when memmove is available to handle the overlap cases?
>
Because memmove will not have the needed semantics in some cases.
In this case, memmove will always keep the original buffer contents
intact. For some algorithms, this is not the needed behavior.
In some cases, one "actually needs" a copy operation that will turn the
output into a repeating pattern of bytes whenever one forward-copies a
chunk of memory over the top of itself.
Also it can be used as a way to implement a "multi byte memset", say for
example, if one wants a fast way to flood-fill a chunk of memory with
0xDEADBEEF or similar, ...
> If one programs in C, one should RTFM, and the FM says
>
> "The memory areas must not overlap"
>
This is the whole point of proposing a "_memlzcpy()" function, in that
it would define this behavior in a particular way for a particular function.
If "memcpy()" or "memmove()" had the needed semantics for this, there
would be no need to define such an extension in the first place.
One could implement it, in theory, as:
void *_memlzcpy(void *dst, void *src, int n)
{
unsigned char *cs, *ct, *cse;
cs=src; ct=dst; cse=cs+n;
while(cs<cse)
*ct++=*cs++;
return(ct);
}
Issue is mostly that this implementation would be unacceptably slow for
some of these use-cases.
On the target I added this as an extension on, implementing the loop
this way is nearly two orders of magnitude slower than doing it using
128-bit load/store operations in 32-byte chunks. So, the "actual"
version will need to use a chunked memory fill in this case (effectively
more like "memset()").
By the time one makes it "not slow", it is an awkward mess.
> As an aside, the latest extensions to the ARMv8 architecture
> include instructions to implement memset and memcpy in an
> hardware efficient manner.
>
> DAGS DDI0487I_a_a-profile_architecture_reference_manual.pdf
> and look at the 30 CPY* instructions (and they still call it RISC :-),
> or the SETP/SETM/SETE instructions.
>
> The key is that the instructions allow the processor to move data
> in the most effective unit-size (e.g. a cache line) rather than
> byte-at-a-time (or the C library mem* assembler functions using
> 64-bit or 128-bit accesses in a loop). It's still a loop, but
> the processor determines how much is moved in each iteration
> rather than the programmer.
>
Can note that in this case, I wasn't necessarily talking about either
x86-64 or ARM...
Granted, the same basic functions could be implemented on both x86 and
ARM without too much issue.
[toc] | [prev] | [next] | [standalone]
| From | scott@slp53.sl.home (Scott Lurndal) |
|---|---|
| Date | 2022-10-04 13:43 +0000 |
| Message-ID | <QLW_K.732364$BKL8.167437@fx15.iad> |
| In reply to | #167956 |
BGB <cr88192@gmail.com> writes: >On 10/3/2022 4:10 PM, Scott Lurndal wrote: >> Kaz Kylheku <864-117-4973@kylheku.com> writes: >>> On 2022-10-03, BGB <cr88192@gmail.com> wrote: >> >> Why would someone want to use memcpy for an overlapping >> move when memmove is available to handle the overlap cases? >> > >Because memmove will not have the needed semantics in some cases. > >In this case, memmove will always keep the original buffer contents >intact. For some algorithms, this is not the needed behavior. > > >In some cases, one "actually needs" a copy operation that will turn the >output into a repeating pattern of bytes whenever one forward-copies a >chunk of memory over the top of itself. What's that, perhaps 0.0001% of the use cases? Roll your own is probably best for that. > >Also it can be used as a way to implement a "multi byte memset", say for >example, if one wants a fast way to flood-fill a chunk of memory with >0xDEADBEEF or similar, ... A simple loop will likely be as fast as any library function. Historically, the Burroughs B4900 mainframe MVA (Move Alpha) instruction could do both of the above overlapping move operations if necessary.
[toc] | [prev] | [next] | [standalone]
| From | BGB <cr88192@gmail.com> |
|---|---|
| Date | 2022-10-04 15:20 -0500 |
| Message-ID | <thi4ku$2o2aa$2@dont-email.me> |
| In reply to | #167959 |
On 10/4/2022 8:43 AM, Scott Lurndal wrote:
> BGB <cr88192@gmail.com> writes:
>> On 10/3/2022 4:10 PM, Scott Lurndal wrote:
>>> Kaz Kylheku <864-117-4973@kylheku.com> writes:
>>>> On 2022-10-03, BGB <cr88192@gmail.com> wrote:
>>>
>
>>> Why would someone want to use memcpy for an overlapping
>>> move when memmove is available to handle the overlap cases?
>>>
>>
>> Because memmove will not have the needed semantics in some cases.
>>
>> In this case, memmove will always keep the original buffer contents
>> intact. For some algorithms, this is not the needed behavior.
>>
>>
>> In some cases, one "actually needs" a copy operation that will turn the
>> output into a repeating pattern of bytes whenever one forward-copies a
>> chunk of memory over the top of itself.
>
> What's that, perhaps 0.0001% of the use cases? Roll your own is probably
> best for that.
>
It is niche, granted.
Still happened commonly enough to where I ended up adding it to my C
library as an extension.
Though, the larger "platform level" API also includes things like LZ
compression and decompression functions (used for various purposes).
>>
>> Also it can be used as a way to implement a "multi byte memset", say for
>> example, if one wants a fast way to flood-fill a chunk of memory with
>> 0xDEADBEEF or similar, ...
>
> A simple loop will likely be as fast as any library function.
>
> Historically, the Burroughs B4900 mainframe MVA (Move Alpha) instruction could do both
> of the above overlapping move operations if necessary.
>
Most of this will be in the context of my ISA (BJX2) at 50 MHz.
Say:
uint32_t *pi;
pi=dest;
for(i=0; i<n; i++)
pi[i]=0xDEADBEEFU;
Will fill at around 25 MB/sec (with the loop spinning at roughly 8 clock
cycles per iteration).
Note: Compiler in this case is basically incapable of unrolling loops.
It can bundle stuff into VLIW bundles, but there is very little to
bundle in these sorts of loops (one needs a bunch of non-dependent ALU
ops and other stuff going on to effectively make much use of VLIW).
With 32 bits per iteration, it can't even reach full DRAM speed.
Also, the CPU is strictly in-order, so a loop with 8 cycles of latency
will always take (at least) 8 cycles to run.
This is part of why naive byte-copy loops are so slow, then one is
looking at around 11 cycles per byte.
But, they could also write:
_mset_uint64(dest, 0xDEADBEEFDEADBEEFULL);
_memlzcpy(dest+8, dest, (n-2)<<2);
And get basically "max speed".
Depends slightly on things like alignment, copy-size, and where it fits
in the cache hierarchy (fastest cases being around 290 MB/sec, for fills
within the L1 cache; this case would drop to around 180 MB/s for the L2
cache, and around 54 MB/s for external DRAM).
Though, even as slow as DRAM is, it is still faster than it would be to
fill the memory 32 bits at a time in a loop.
RAM is pretty slow in this case: Roughly 100 MB/s for unidirectional
load/store, and 56 MB/s for Swap (conjoined Load+Store). For "memset()"
style tasks, the L2 cache primarily does Swap operations.
Where, in this case, a 64-bit store would give an 8-byte alignment,
which allows using 128-bit memory operations (a 4-byte alignment would
drop it to around 160 MB/s due to needing to drop back to 64-bit
operations).
Where in this case, the ISA is limited to only one memory Load/Store per
clock-cycle (1 throughput, 3 latency, for both 64 and 128 bit Load/Store
ops).
Whereas, say:
uint64_t *pli, *plie;
pli=dest; n1=n>>1; plie=pli+n1;
while(pli<plie)
{
pli[0]=0xDEADBEEFDEADBEEFULL;
pli[1]=0xDEADBEEFDEADBEEFULL;
pli[2]=0xDEADBEEFDEADBEEFULL;
pli[3]=0xDEADBEEFDEADBEEFULL;
pli+=4;
}
Will reach roughly 160 MB/s, but now "what if n was not an even multiple
of 8 elements?", etc...
If larger than the L1 size, it will drop down to L2 and then DRAM speeds.
Granted, one could modify the loop further:
uint64_t *pli, *plie;
uint64_t fill;
fill=0xDEADBEEFDEADBEEFULL;
pli=dest; n1=n>>1; plie=pli+n1;
while(pli<plie)
{
pli[0]=fill; pli[1]=fill;
pli[2]=fill; pli[3]=fill;
pli[4]=fill; pli[5]=fill;
pli[6]=fill; pli[7]=fill;
pli+=8;
}
Roughly 14 cycles per iteration, 64 bytes per iteration, so ~ 228 MB/s.
But, what else could one do:
uint128_t *pli, *plie;
uint128_t fill;
fill=0xDEADBEEFDEADBEEFDEADBEEFDEADBEEFUI128;
pli=dest; n1=n>>2; plie=pli+n1;
while(pli<plie)
{
pli[0]=fill; pli[1]=fill;
pli[2]=fill; pli[3]=fill;
pli[4]=fill; pli[5]=fill;
pli[6]=fill; pli[7]=fill;
pli+=8;
}
Now pushes 128 bytes per loop iteration, and can making some use of
128-bit memory store operations (and can hit the 290 MB/s limit).
Obvious problem: Using 128-bit types is no longer standard C.
Also, on this target, this will break if the destination is not properly
aligned, and while you could write:
__unaligned uint128_t *pli, *plie;
This falls back to using smaller (64-bit) stores internally (for
technical reasons, the ISA in question imposes a 64-bit alignment for
128-bit Load/Store operations).
Theoretical hard-limit for 128-bit stores is 800MB/s at 50MHz, but this
can't really be achieved in practice (the closest I have gotten in
practice is around 470MB/s, when storing at 512 bytes per loop iteration).
For most uses, a memory fill loop that move 64 bytes at a time and hits
a limit of ~ 300 MB/s tend to be a little more practical (fast enough to
saturate DRAM and L2, so "good enough").
On my desktop PC (Zen+, 3.7 GHz), can generally get:
~ 3.8 GB/s, for large fills (4MB).
~ 7.4 GB/s, for medium fills (128K).
~ 12 GB/s, for small fills (8K).
The relative impact of element size or loop structure for the memory
stores seems to be a lot smaller on this machine (likely because of OoO
and similar).
Though, naive byte copy loops are still relatively slow, even on x86-64.
LZ decompressors are harder, usually harder to get them much past around
2 GB/s or so for typical data (for an LZ4 style compression format).
For anything with a Huffman stage, hard to get much past around 600-800
MB/s or so (single threaded).
Hiding this stuff behind a function is "usually" preferable, and then
the program can hopefully avoid a big mess along the lines of, say:
#ifdef _MSC_VER
... MSVC stuff
#endif
#ifdef __GNUC__
... GCC stuff
#endif
#ifdef _M_X64 //MSVC
...
#endif
#ifdef _M_X86 //MSVC
...
#endif
#ifdef __i386__ //GCC or Clang
...
#endif
#ifdef __x86_64__ //GCC or Clang
...
#endif
#ifdef _M_ARM //ARM + MSVC
...
#endif
#ifdef __arm__ //ARM + GCC
...
#endif
#ifdef __BJX2__ //my ISA, assumes it is BGBCC
...
#endif
..
Where, stuff like this is preferably kept to a minimum, and tends to
turn programs into a big hairy mess.
One usual option is to put a lot of the "common hair" into files that
one copy/pastes from one project to another, but this still kinda sucks.
And, what usually goes in these blocks:
Wrappers for loading/storing values from pointers;
Specialized memory copy stuff;
...
Basically, some amount of the stuff I proposed in the OP.
The malloc stuff is its own category, mostly because of annoyances where
baseline malloc isn't really sufficient, and so the program tends to
need to implement its own memory allocator.
[toc] | [prev] | [next] | [standalone]
| From | BGB <cr88192@gmail.com> |
|---|---|
| Date | 2022-10-05 03:58 -0500 |
| Message-ID | <thjh1b$2uln1$2@dont-email.me> |
| In reply to | #167962 |
On 10/4/2022 3:20 PM, BGB wrote:
> On 10/4/2022 8:43 AM, Scott Lurndal wrote:
>> BGB <cr88192@gmail.com> writes:
>>> On 10/3/2022 4:10 PM, Scott Lurndal wrote:
>>>> Kaz Kylheku <864-117-4973@kylheku.com> writes:
>>>>> On 2022-10-03, BGB <cr88192@gmail.com> wrote:
>>>>
>>
>>>> Why would someone want to use memcpy for an overlapping
>>>> move when memmove is available to handle the overlap cases?
>>>>
>>>
>>> Because memmove will not have the needed semantics in some cases.
>>>
>>> In this case, memmove will always keep the original buffer contents
>>> intact. For some algorithms, this is not the needed behavior.
>>>
>>>
>>> In some cases, one "actually needs" a copy operation that will turn the
>>> output into a repeating pattern of bytes whenever one forward-copies a
>>> chunk of memory over the top of itself.
>>
>> What's that, perhaps 0.0001% of the use cases? Roll your own is
>> probably
>> best for that.
>>
>
> It is niche, granted.
>
> Still happened commonly enough to where I ended up adding it to my C
> library as an extension.
>
> Though, the larger "platform level" API also includes things like LZ
> compression and decompression functions (used for various purposes).
>
I noted that the version that existed online was a few months out of
date, so stuck the code for a newer version on pastebin:
https://pastebin.com/6TG2DbEc
While I guess one could object to the wanton abuse of pointers in this
code, the target ISA allows 64-bit (and smaller) pointers to be
misaligned, and my compiler doesn't do "Strict Aliasing" / TBAA by
default (otherwise, some of this wouldn't really fly in a compiler which
does TBAA).
Generally, in this case, abusing the pointers is generally the fastest
way to get this stuff shuffled around in memory.
In some other parts of the codebase, there are wrappers around the raw
pointer casts and derefs.
In a few of these cases, constant "memcpy()" could be used, which BGBCC
will optimize into bare loads/stores internally, so could be worth
considering.
Though, one still needs to be sparing with memcpy, since how effectively
it may be optimized away is a bit hit or miss (and it will still tend to
end the current basic-block, which will in turn may still cause the
compiler to spill all the not-statically-assigned variables to the stack
even in cases where it does turn it into memory ops; sometimes is will
spill the registers just to reload them again so that it can do a memory
op, then discard the registers, only reload the registers again from the
stack after the "call" has returned).
Though, this is partly because the front-end doesn't necessarily know in
advance whether the backend will treat it like a built-in or emit a
function call (for functions known to be intrinsics, it can use a
different operator and sidestep the spill-and-reload parts implied by a
normal function call).
Bare pointer cast and de-reference at least manages to avoid this
particular issue...
In these functions, the "register" keyword isn't used mostly because,
as-is, my compiler is mostly smart enough at this point to figure out
which variables to prioritize (usually), and otherwise "register" serves
as a hint that the compiler should assume that the function is on the
hot path (and using this keyword too casually may have a detrimental
effect on code-density).
Can't expect too many "clever" optimizations though, my compiler isn't
particularly clever about this stuff...
As can be observed, most of this isn't ASM (the actual implementation of
the "memcpy()" function in this case is written in ASM though).
>
>>>
>>> Also it can be used as a way to implement a "multi byte memset", say for
>>> example, if one wants a fast way to flood-fill a chunk of memory with
>>> 0xDEADBEEF or similar, ...
>>
>> A simple loop will likely be as fast as any library function.
>>
>> Historically, the Burroughs B4900 mainframe MVA (Move Alpha)
>> instruction could do both
>> of the above overlapping move operations if necessary.
>>
>
> Most of this will be in the context of my ISA (BJX2) at 50 MHz.
>
> Say:
> uint32_t *pi;
> pi=dest;
> for(i=0; i<n; i++)
> pi[i]=0xDEADBEEFU;
>
> Will fill at around 25 MB/sec (with the loop spinning at roughly 8 clock
> cycles per iteration).
>
> Note: Compiler in this case is basically incapable of unrolling loops.
> It can bundle stuff into VLIW bundles, but there is very little to
> bundle in these sorts of loops (one needs a bunch of non-dependent ALU
> ops and other stuff going on to effectively make much use of VLIW).
>
> With 32 bits per iteration, it can't even reach full DRAM speed.
>
>
> Also, the CPU is strictly in-order, so a loop with 8 cycles of latency
> will always take (at least) 8 cycles to run.
>
> This is part of why naive byte-copy loops are so slow, then one is
> looking at around 11 cycles per byte.
>
>
>
> But, they could also write:
> _mset_uint64(dest, 0xDEADBEEFDEADBEEFULL);
> _memlzcpy(dest+8, dest, (n-2)<<2);
>
> And get basically "max speed".
>
>
> Depends slightly on things like alignment, copy-size, and where it fits
> in the cache hierarchy (fastest cases being around 290 MB/sec, for fills
> within the L1 cache; this case would drop to around 180 MB/s for the L2
> cache, and around 54 MB/s for external DRAM).
>
> Though, even as slow as DRAM is, it is still faster than it would be to
> fill the memory 32 bits at a time in a loop.
>
> RAM is pretty slow in this case: Roughly 100 MB/s for unidirectional
> load/store, and 56 MB/s for Swap (conjoined Load+Store). For "memset()"
> style tasks, the L2 cache primarily does Swap operations.
>
>
> Where, in this case, a 64-bit store would give an 8-byte alignment,
> which allows using 128-bit memory operations (a 4-byte alignment would
> drop it to around 160 MB/s due to needing to drop back to 64-bit
> operations).
>
> Where in this case, the ISA is limited to only one memory Load/Store per
> clock-cycle (1 throughput, 3 latency, for both 64 and 128 bit Load/Store
> ops).
>
>
> Whereas, say:
> uint64_t *pli, *plie;
> pli=dest; n1=n>>1; plie=pli+n1;
> while(pli<plie)
> {
> pli[0]=0xDEADBEEFDEADBEEFULL;
> pli[1]=0xDEADBEEFDEADBEEFULL;
> pli[2]=0xDEADBEEFDEADBEEFULL;
> pli[3]=0xDEADBEEFDEADBEEFULL;
> pli+=4;
> }
>
> Will reach roughly 160 MB/s, but now "what if n was not an even multiple
> of 8 elements?", etc...
>
>
> If larger than the L1 size, it will drop down to L2 and then DRAM speeds.
>
>
> Granted, one could modify the loop further:
> uint64_t *pli, *plie;
> uint64_t fill;
>
> fill=0xDEADBEEFDEADBEEFULL;
> pli=dest; n1=n>>1; plie=pli+n1;
> while(pli<plie)
> {
> pli[0]=fill; pli[1]=fill;
> pli[2]=fill; pli[3]=fill;
> pli[4]=fill; pli[5]=fill;
> pli[6]=fill; pli[7]=fill;
> pli+=8;
> }
>
> Roughly 14 cycles per iteration, 64 bytes per iteration, so ~ 228 MB/s.
>
> But, what else could one do:
> uint128_t *pli, *plie;
> uint128_t fill;
>
> fill=0xDEADBEEFDEADBEEFDEADBEEFDEADBEEFUI128;
> pli=dest; n1=n>>2; plie=pli+n1;
> while(pli<plie)
> {
> pli[0]=fill; pli[1]=fill;
> pli[2]=fill; pli[3]=fill;
> pli[4]=fill; pli[5]=fill;
> pli[6]=fill; pli[7]=fill;
> pli+=8;
> }
>
> Now pushes 128 bytes per loop iteration, and can making some use of
> 128-bit memory store operations (and can hit the 290 MB/s limit).
>
> Obvious problem: Using 128-bit types is no longer standard C.
>
>
> Also, on this target, this will break if the destination is not properly
> aligned, and while you could write:
> __unaligned uint128_t *pli, *plie;
>
> This falls back to using smaller (64-bit) stores internally (for
> technical reasons, the ISA in question imposes a 64-bit alignment for
> 128-bit Load/Store operations).
>
> Theoretical hard-limit for 128-bit stores is 800MB/s at 50MHz, but this
> can't really be achieved in practice (the closest I have gotten in
> practice is around 470MB/s, when storing at 512 bytes per loop iteration).
>
> For most uses, a memory fill loop that move 64 bytes at a time and hits
> a limit of ~ 300 MB/s tend to be a little more practical (fast enough to
> saturate DRAM and L2, so "good enough").
>
>
>
>
> On my desktop PC (Zen+, 3.7 GHz), can generally get:
> ~ 3.8 GB/s, for large fills (4MB).
> ~ 7.4 GB/s, for medium fills (128K).
> ~ 12 GB/s, for small fills (8K).
>
> The relative impact of element size or loop structure for the memory
> stores seems to be a lot smaller on this machine (likely because of OoO
> and similar).
>
> Though, naive byte copy loops are still relatively slow, even on x86-64.
>
>
>
> LZ decompressors are harder, usually harder to get them much past around
> 2 GB/s or so for typical data (for an LZ4 style compression format).
>
> For anything with a Huffman stage, hard to get much past around 600-800
> MB/s or so (single threaded).
>
>
> Hiding this stuff behind a function is "usually" preferable, and then
> the program can hopefully avoid a big mess along the lines of, say:
>
> #ifdef _MSC_VER
> ... MSVC stuff
> #endif
>
> #ifdef __GNUC__
> ... GCC stuff
> #endif
>
> #ifdef _M_X64 //MSVC
> ...
> #endif
>
> #ifdef _M_X86 //MSVC
> ...
> #endif
>
> #ifdef __i386__ //GCC or Clang
> ...
> #endif
>
> #ifdef __x86_64__ //GCC or Clang
> ...
> #endif
>
> #ifdef _M_ARM //ARM + MSVC
> ...
> #endif
>
> #ifdef __arm__ //ARM + GCC
> ...
> #endif
>
> #ifdef __BJX2__ //my ISA, assumes it is BGBCC
> ...
> #endif
>
> ..
>
> Where, stuff like this is preferably kept to a minimum, and tends to
> turn programs into a big hairy mess.
>
>
> One usual option is to put a lot of the "common hair" into files that
> one copy/pastes from one project to another, but this still kinda sucks.
>
>
> And, what usually goes in these blocks:
> Wrappers for loading/storing values from pointers;
> Specialized memory copy stuff;
> ...
>
> Basically, some amount of the stuff I proposed in the OP.
>
>
> The malloc stuff is its own category, mostly because of annoyances where
> baseline malloc isn't really sufficient, and so the program tends to
> need to implement its own memory allocator.
>
>
>
[toc] | [prev] | [next] | [standalone]
| From | Kaz Kylheku <864-117-4973@kylheku.com> |
|---|---|
| Date | 2022-10-04 16:56 +0000 |
| Message-ID | <20221004011719.833@kylheku.com> |
| In reply to | #167956 |
On 2022-10-04, BGB <cr88192@gmail.com> wrote: > In some cases, one "actually needs" a copy operation that will turn the > output into a repeating pattern of bytes whenever one forward-copies a > chunk of memory over the top of itself. You mention this in the context of LZ77 deflate, but I don't see any such thing in zlib sources. There is a zmemcpy which in some cases is just a macro for memcpy. Is Adler missing some clue or something? > Also it can be used as a way to implement a "multi byte memset", say for > example, if one wants a fast way to flood-fill a chunk of memory with > 0xDEADBEEF or similar, ... A multi-byte memset that is reading from the area where it is writing seems inefficient, particularly if the memory is cache-cold, since it will actually be sucking the memory into the processor's caches only to turn around a blast it out again. You really want an actual multi-byte memset for that use case. -- TXR Programming Language: http://nongnu.org/txr Cygnal: Cygwin Native Application Library: http://kylheku.com/cygnal
[toc] | [prev] | [next] | [standalone]
| From | BGB <cr88192@gmail.com> |
|---|---|
| Date | 2022-10-04 18:30 -0500 |
| Message-ID | <thifob$2p2ns$1@dont-email.me> |
| In reply to | #167961 |
On 10/4/2022 11:56 AM, Kaz Kylheku wrote:
> On 2022-10-04, BGB <cr88192@gmail.com> wrote:
>> In some cases, one "actually needs" a copy operation that will turn the
>> output into a repeating pattern of bytes whenever one forward-copies a
>> chunk of memory over the top of itself.
>
> You mention this in the context of LZ77 deflate, but I don't see
> any such thing in zlib sources. There is a zmemcpy which
> in some cases is just a macro for memcpy.
>
> Is Adler missing some clue or something?
>
No zlib here, rather a custom implementation.
One needs Deflate mostly for things like ZIP and PNG, but one doesn't
necessarily need to use zlib to do so (nor does one need libpng for PNGs).
Well, and in this case, also not running on a conventional OS.
No Linux or Windows in this case, rather "TestKern", which is sort of like:
Uses a lot of file-formats and other stuff borrowed from Windows
Uses PE/COFF, RIFF based formats, ...
Uses an API design more modeled after POSIX
General "architecture" generally has more in common with MS-DOS at this
stage (memory protection is borderline non existent; does more or less
have virtual memory working, but all of the program instances still
currently run in a single shared virtual address space).
Also, like DOS (and unlike Linux or Windows), doesn't have preemptive
multitasking yet. Had written some code for this, but it isn't yet used,
and work is still needed for "processes" to be a thing. Cooperative
multithreading isn't exactly the same.
Porting software to it is a little bit of a hassle (things like
"./configure" aren't going to work when one doesn't even have Bash).
Porting something like Linux or BSD or similar would probably be better,
but porting these to my ISA look like probably an uphill battle (and
porting the GNU userland isn't really going to work out well without GCC
support for this architecture, ...).
As for the way _memlzcpy fits in with LZ77, it is typically that matches
are expressed as a backwards distance and a length. Where, if the
distance is less than the length, one gets a repeating pattern.
So:
ct=_memlzcpy(ct, ct-dist, len);
Can express the typical LZ style match-copy operation.
Say, for decoding an LZ4 style format, one could write:
ct=dest; cs=src; cse=cs+csize;
while(cs<cse)
{
i=*cs++;
rl=i>>4; ml=(i&15)+4;
if(rl==15)
{
i=*cs++;
while(i==255)
{ rl+=i; i=*cs++; }
rl+=i;
}
ct=_memlzcpyf(ct, cs, rl);
cs+=rl;
if(cs>=cse)
break;
md=_mget_uint16le(cs);
cs+=2;
if(ml==19)
{
i=*cs++;
while(i==255)
{ ml+=i; i=*cs++; }
ml+=i;
}
ct=_memlzcpyf(ct, ct-md, ml);
ct+=ml;
}
Decided to leave out going a bunch into stuff related to LZ compressors.
They are used for various purposes, among other things, using
compression as a way to read data from the SDcard faster (at 12.5 MHz,
the SDcard only does IO at around 1.5 MB/s in SPI mode).
The main target I am dealing with for this is mostly on my BJX2 ISA,
which is basically a 64-bit 3-wide VLIW, generally runs on FPGA,
generally at 50MHz.
In some areas, it is kinda meh:
Runs Doom at ~ 15-20 fps;
Runs Hexen at ~ 8-10 fps;
Runs ROTT at ~ 10-12 fps;
SW Quake at ~ 2-4 fps;
GLQuake at ~ 5-8 fps;
At present, only gets ~ 74k in Dhrystone (~ 0.84 DMIPS/MHz, *1);
...
But, it does a "surprisingly passable" job at things like software
OpenGL (*2) rasterization (and a lot of "my own stuff" does rendering
using the software rasterized OpenGL; also a custom implementation
optimized for this ISA, with a fair chunk written in ASM).
*1: At Dhrystone, it seems that RISC-V gets better DMIPS/MHz scores.
I suspect some of this is due to GCC being a lot more "clever" than my
compiler (BGBCC).
Arguably, RISC-V is still a much better option in terms of "being
practical for general use".
Though, can generally pass timing at higher clock speeds than the SweRV
core, even if the DMIPS/MHz score is worse. Both need roughly similar
class FPGAs (XC7S50 or XC7A100 or similar). Had noted that internal
architecture was very different. Though, have noted that Dhrystone on
simpler 1-wide scalar RISC-V cores seems to be closer to around 0.6
DMIPS/MHz (rather than ~ 1.4).
The pipeline is very different, they seem to have the instruction
pipeline and memory load-store as two independent components (connected
via a FIFO interface or similar).
In my case, the L1 caches and pipeline operate in lockstep. So, if an L1
miss happens, the pipeline stalls until the situation is resolved (and
all instructions in a VLIW bundle advance strictly in lockstep).
Extracting usable ILP from a program is left pretty much entirely to the
compiler (and/or the person writing ASM code for it).
*2: It implements the OpenGL API, more or less, but is basically a
software renderer on the backend, and uses affine filtering (with
dynamic tessellation), so tends to generate output that kinda more
resembles something like the original PlayStation than a modern GPU.
Software Quake is around 2-4 fps, GLQuake is around 5-8.
Had worked some on trying to porting Quake 3 Arena to it, but this
fizzled out as Q3A is both memory hungry and very unlikely to be usable.
Partly this is because an OpenGL style rasterizer can make slightly more
effective use of the CPU's VLIW capabilities.
Ironically, I do have a small custom "Minecraft like" 3D engine running
on it (though, staying above 5 fps requires limiting it to a 12 meter
draw distance, which kinda sucks).
Also ironically, still faster than trying to run "actual" Minecraft with
a similar draw distance on a laptop from 2003 (which has 36x higher
clock speed). Though, this laptop is plenty fast enough to run Quake and
similar.
Can do video playback semi passable. But, it is a balancing act between
computational cost of the video decoding and keeping the bitrate low
enough that it doesn't get stuck on IO bandwidth (some of the "classic"
codecs like CRAM or RPZA use need too much IO bandwidth to get the video
data off the SDcard).
Was generally having best results in this case with hybrid CC/VQ codecs
with an LZ post-compression stage.
Had observed that it is fast enough at JPEG decoding, that it is at
least possible that an MPEG style decoder could be used (not tested
yet). Main "slow parts" of the JPEG decoding in this case being the
Huffman/VLC decoding, and writing stuff to the output framebuffer
(things like IDCT and YCbCr->RGB transform mapping "reasonably well" to
VLIW).
Though, an MPEG-like codec would likely be limited to 160x100 or
similar, as 320x200 is likely to have more computational cost than the
CPU could deal with at 50 MHz.
With the VQ+LZ approach was generally able to manage 320x200 video.
>> Also it can be used as a way to implement a "multi byte memset", say for
>> example, if one wants a fast way to flood-fill a chunk of memory with
>> 0xDEADBEEF or similar, ...
>
> A multi-byte memset that is reading from the area where it is writing
> seems inefficient, particularly if the memory is cache-cold,
> since it will actually be sucking the memory into the processor's
> caches only to turn around a blast it out again.
>
> You really want an actual multi-byte memset for that use case.
>
That was also a possible consideration. Depends mostly on use case.
Main hassle with a multi-byte memset is that one (potentially) needs
multiple versions for each fill size.
But, yeah, something like _memset16, _memset32, or _memset64 could also
address this use-case.
Though, a multibyte memset would make things like alignment a little
easier (in my case, calls like "malloc()" always return memory with a
16-byte alignment).
..
[toc] | [prev] | [next] | [standalone]
| From | BGB <cr88192@gmail.com> |
|---|---|
| Date | 2022-10-03 18:19 -0500 |
| Message-ID | <thfqof$2b9cf$1@dont-email.me> |
| In reply to | #167953 |
On 10/3/2022 3:40 PM, Kaz Kylheku wrote: > On 2022-10-03, BGB <cr88192@gmail.com> wrote: >> On 10/2/2022 10:38 PM, Tim Rentsch wrote: >>> BGB <cr88192@gmail.com> writes: >>> >>>> There are some things that come up often that it might be "useful" if >>>> they could be supported in a more portable ways. >>>> >>>> [ ... ] >>>> >>>> Any thoughts?... >>> >>> None of these is suitable for inclusion in the ISO C standard. >> >> Possibly. >> >> >> As noted, this was more an "idle wish list", based mostly on stuff that >> comes up a lot in my experience, not really a proposal that this stuff >> be added (as-is) to the C standard. > > I don't know why you would even wish to have most of that stuff in the > standard. > > The standard would be objectively worse, even for you, whenever > you're working on anything but the one program where you need any > of it. > I didn't originally say anything about wanting to add any of this to the C standard in the first place... I would just prefer if it could be "more portable", which could be achieved easily enough in a "de-facto" way. >> A few of them, such as _msize(), exist in MSVCRT, and is functionally >> equivalent to malloc_usable_size() in GLIBC. > > _msize doesn't return the size that was passed to malloc; it returns > some rounded up size. Still that can be useful. > Yes, I mentioned this in the OP. The proposed behavior *was* that it would return the padded-up size. > Code which manages a buffer that grows when it becomes full > tracks the allocated size from the actual filled size. With this > function, you don't have to waste space storing the allocated > size and keeping it up-to-date: you just retrieve it. Moreover, > you use the full underlying size without any waste. > > The function would have to be specified such that if you malloc(42), > and then malsize(ptr) yields 64, it becomes legitimate for you > to make use of all the bytes bytes 0 to 63. > > Moreover, it would have to be specified that malsize(ptr) is called, and > returns some value, then it must always return a value at least as large > for ptr, regardless of any memory allocations or deallocations that take > place.. The memory indicated by that size must really belong to the > allocated object. > All this was already implicit in the original idea. I wasn't claiming: p=malloc(42); sz=_msize(p); Should have sz==42, merely sz>=42 ... Usually, because the allocator does tend to pad things up internally, and also we don't usually want to preserve the exact size of the allocation in the first place (it is not usually needed, and has a non-zero cost needed to store it). >> Like, say, for example, what if the C library had not provided >> "memcpy()" and similar, and nearly every application was left to roll >> their own, often doing so poorly. > > Sure, but how many need a memcpy that allows overlap, but if the > overlap is in the wrong direction, it then repeats a byte? > Yes. In this case: _memlzcpy(dst+1, dst, 256); Would be semantically equivalent to: memset(dst+1, *dst, 256); If the delta is 2 bytes, it will repeat those 2 bytes, or 3 bytes will repeat a 3 byte pattern, etc. > A memcpy that allows overlap, if the second operand has a higher > address than the first, would be mildly useful. However, > if we say that the second address must be higher, that can be > satisfied by it being higher only by a byte. > > The motivation for that function is that a simple loop can perform the > copy, which sweeps over both operands in order of increasing address. > However, it can only work reliably if the transfer unit's width > is no larger than the displacement between the two buffers. > So in the case of a one byte difference, the loop must transfer > a byte at a time. > Not necessarily, in a typical implementation, it can be turned into a pattern-fill register, which is then written to memory in a single larger block. We don't want to fall back to a "one byte at a time" copy in this case, because this is slow; but it is necessary to have the same output *as-if* it had been a "byte at a time" copy operation. > In cases when the address delta can't be deduced at compile time, > that function would have to switch on the delta size, and say > handle the 1, 2, 4 and 8 byte cases specially. Plus handle the > alignment cases and all that. > Yes. Typically, it needs to specially handle all 1-15 cases, with 16+ bytes cases typically able to fall back to the normal SIMD based copy, and another (slightly faster) SIMD loop usually at 32 or 64 bytes. Non-power-of-2 sizes (3/5/7/...) get a little more complicated, but would still need to be handled. Usual options for this are one of: Pattern fill has a non-power-of-2 stepping, using misaligned memory stores; Multiple pattern fills are generated, with the fill alternating between fill patterns based on a modulo. The latter case is more limited in scope (doesn't scale very well), so the former is typically what is used (say, each store is 128 bits, but the destination pointer is advanced by 13 or 15 bytes or similar each time). Despite typically needing to pay a penalty for misaligned SIMD store, this tends to work out faster on-average than the other option (as well as being a lot simpler in terms of the required "big blobs of ASM"). > It's not clear that it would end up winning very much over memmove. > Programmers who want the most performance out of memcpy just > make it non-overlapping. > memmove has the wrong semantics for cases where one actually needs the preceding behavior. The point of "_memlzcpy()" is partly because: In some cases, one needs these particular semantics for overlapping copies (so paying these costs is unavoidable); One doesn't want to make normal "memcpy()" slower by asking it to detect/handle scenarios that are N/A to most normal uses of memcpy. So, one ends up with programs needing to implement their own version, with it often either being slow or turning into an ugly mess of platform-specific code. As implied by the name, one of the major cases where this comes up tends to be things like LZ77 decompressors (such as: LZ4, Deflate, etc). For some decompressors (such as LZ4, or my own RP2 format), copying matches around in the "sliding window" tends to be the majority of the clock-cycle budget for these tasks. > Versions of memcpy and memmove which allow the application to > specify the alignment (whereby the application ensures that > the promised alignemnt is true) would be useful: > > /* array copy, array move */ > > /* non-overlapping operands. > both operand pointers aligned to elem_size, else UB. */ > > arrcpy(dest, src, elem_cnt, elem_size) > > /* Possibly overlapping operands. > both operand pointers aligned to elem_size, else UB. */ > > arrmove(dest, src, elem_cnt, elem_size) > > Copy operations that don't have to handle run-time alignment cases, and > odd leftover sizes, could likely be implemented faster. > Yes, granted. Possible option is specifying both the size and alignment... Well, and/or doing the more naive solution and providing functions for each (power of 2) combination of size and alignment up to a certain range. _arrmcpy_16x8(dst, src, cnt); //16-byte items with 8-byte alignment _arrmcpy_64x16(dst, src, cnt); //64-byte items with 16-byte alignment ... > The elem_size expression is often a constant expression, in which cases > the compiler can rewrite the call to use a function which handles that > transfer unit size (or multiples), without worrying about alignment or > partial transfer units at the end. > > ISO C (since 99) has something like this, for wchar_t: wmemcpy > and wmemmove. The above functions would just generalize that. > Granted. My compiler also does some similar stuff internally for things like struct copying, since it statically knows the size and alignment of the struct or array. Otherwise, "memcpy()" is also specialized in some cases as well, since the compiler can "see" the types and alignments of the passed in pointers, and if the copy size is constant, and so may special case some of this (only producing a "true" memcpy call as a fallback case). In some other cases, it might turn it into bare loads and stores. Likewise, "memset()" may also get similar treatment. Also can note that for a lot of this, I am dealing with a 64-bit VLIW architecture clocked at 50 MHz, where "little things" like this can have a fairly drastic impact on performance. Likewise, the performance difference between a SIMD copy loop, and a "byte at a time" copy loop, is nearly 2 orders of magnitude. Where, say (for a 50MHz CPU core): Byte-at-a-time copy loop runs at roughly 4.5 MB/s; SIMD copy loop can run closer to around 290 MB/s. Partly as the CPU in this case is pretty much entirely devoid of the sorts of OoO cleverness one might expect on x86 and friends.
[toc] | [prev] | [next] | [standalone]
| From | Tim Rentsch <tr.17687@z991.linuxsc.com> |
|---|---|
| Date | 2022-11-19 06:54 -0800 |
| Message-ID | <86a64nc7vi.fsf@linuxsc.com> |
| In reply to | #167947 |
BGB <cr88192@gmail.com> writes: > On 10/2/2022 10:38 PM, Tim Rentsch wrote: > >> BGB <cr88192@gmail.com> writes: >> >>> There are some things that come up often that it might be "useful" if >>> they could be supported in a more portable ways. >>> >>> [ ... ] >>> >>> Any thoughts?... >> >> None of these is suitable for inclusion in the ISO C standard. > > Possibly. Definitely. > This is stuff that comes up a lot, and almost every non-trivial > program needs to implement a lot of this itself [...] No, it doesn't. Some programs do. Certainly it is not the case that almost every non-trivial program does. > Like, say, for example, what if the C library had not provided > "memcpy()" and similar, [...] There are obvious differences between memcpy() and the interfaces you describe. Those differences explain why memcpy() should be included in the C standard library, and the described interfaces should not. > [...] Your thoughts are rather scattered. If you want to make a serious suggestion, you should focus on exactly what it is you want to suggest, and talk about that, and nothing else.
[toc] | [prev] | [next] | [standalone]
| From | gazelle@shell.xmission.com (Kenny McCormack) |
|---|---|
| Date | 2022-10-03 09:29 +0000 |
| Message-ID | <thea1e$28v41$1@news.xmission.com> |
| In reply to | #167946 |
In article <86y1txinka.fsf@linuxsc.com>, Tim Rentsch <tr.17687@z991.linuxsc.com> wrote: >BGB <cr88192@gmail.com> writes: > >> There are some things that come up often that it might be "useful" if >> they could be supported in a more portable ways. >> >> [ ... ] >> >> Any thoughts?... > >None of these is suitable for inclusion in the ISO C standard. Wow. What a charming, helpful, forward-looking response. -- Hindsight is (supposed to be) 2020. Trumpers, don't make the same mistake twice. Don't shoot yourself in the feet - and everywhere else - again!.
[toc] | [prev] | [standalone]
Back to top | Article view | comp.lang.c
csiph-web