Skip to main content
GameDev.net gamedev.net
🔒 Locked

Fast Approximation to memcpy()

Started by L. Spiro Oct 11, 2016 at 12:51 PM 29 replies 15k views
Original Post
L. Spiro
L. Spiro
After creating fast approximations for sin() and cos(), I found that one of the heaviest functions on a per-frame basis is std::memcpy() (and std::memset()).
This is bothersome, so I decided to work on a cheap approximation for these routines too.

Post your fastest, cheapest approximations to std::memcpy() here!


L. Spiro
I restore Nintendo 64 video-game OST’s into HD! https://www.youtube.com/channel/UCCtX_wedtZ5BoyQBXEhnVZw/playlists?view=1&sort=lad&flow=grid
_WeirdCat_
_WeirdCat_

i wont even start to code that in asm: wasnt that just a loop, asm loop ?

like in single c++

for (int i=0; i < len; i++)

destination = source;

you anyway pass an array of addresses of the source

the less instructions you write the better

:wub:

somehow i cant imagine to just throw set of bytes, into other memory region

Kylotan
Kylotan
void kyloMemCpy(void *dest, void *src, size_t n)
{
char *cdest = (char *)dest;
for (int i=0; i
cdest = 0; // most memory gets initialised to zero by something, so statistically this is usually correct
}
21st Century Moose
21st Century Moose

Since you asked for an "approximation" to memcpy:

void *src; // assumed that this is set to something valid

void *dst; // assumed that this is set to something valid

dst = src;

There you go! Approximately equivalent to memcpy!

Direct3D has need of instancing, but we do not. We have plenty of glVertexAttrib calls. 
samoth
samoth

making a good memcpy or memset is excruciatingly hard (huh, did I spell that right?) mostly because you don't know in advance.

Wait, you don't know? What is it you don't know? Well... everything.

You don't know on which CPU your code will run, you don't know how large the block will be (not at the time of writing the memcpy function anyway!) and you don't know what access pattern is going to be used (nor is there a way to communicate this over that API).

On some CPUs, something like REP STOS is the fastest way up to about half a kilobyte or a kilobyte. On others, a simple 4-times unrolled loop in C copying a uint64_t at a time is faster. At some point, it turns around. On some CPUs, SSE4.1 string instructions are fastest, on some they are not, and on some they don't even exist. For blocks greater than so-and-so-many bytes, using SSE registers either way (even SSE2) is faster than registers. Depending on whether or not you are soon reading back the data (e.g. fill a buffer handed to GPU), using write-combining writes is faster and more efficient, but in every other case it's slower. On some CPUs, you must use several threads to saturate memory bandwidth. Prefetching is advantageous, sometimes. And sometimes it is only extra instructions which are slower overall. Usually, almost always, your intuition tells you the wrong thing and you would have been better not prefetching at all.

SSE optimized versions may or may not overwrite without causing a fault if you are being careless, and with no easy way of detecting the problem. Admitted, non-SSE code can do that as well, but you still have a good chance of getting a crash (which is a good thing!). SSE writes are pretty much guaranteed to hide the issue.

Why? Well, because it so happens that your SSE writes are always properly aligned, and thus always fit snugly into a page (because 4096 is a greater power-of-two than 16), so you don't get a segmentation fault if your last write accidentially garbled some bytes past the end. All you get is garbled data, and maybe a crash in a different, unrelated piece of code much later, but never a segfault when it should happen.

I would not be surprised if there even existed systems where remapping pages and thus pulling a page from the operating system's zero page pool was the fastest possible way of zeroing memory (and I wouldn't be surprised if there existed systems which abused DMA in some way (network card, graphics card? disk controller!?) to do memset or memcpy in hardware).

On almost all systems, for all "not terribly huge" sizes, the badly predictable branches necessary to find the best, optimal solution for each case are almost as expensive as the savings, so unless you are operating on a small compiletime-known size, you're kinda lost... and in that case, the compiler would replace the vanilla library call with an optimized inline version anyway. Any no-shit compiler in a release build would, at least.

So, all in all... it's a bit of a totally-no-fun endeavour. I'm just using the C standard library's function. Which, hopefully, is always good enough.

samoth
samoth

Just map the same physical region to another virtual address. Same content, zero copy. Different addresses!

Hahaha, that too! :D

This is a treacherous way of doing it, though. Unless you map the memory CoW, writing to one block of memory will (obviously) modify the other. Which is almost certainly not what the unwitty observer would expect to happen. But if you map it CoW, you have the same insidious behaviour that people writing servers experience with fork.

You fork, this took slightly under half a millisecond, and it's all fine and dandy, everything just works, life is good. And then either the parent or the child process writes to memory. Oh fuck... suddenly everything is so slow, I cannot understand why there are so many page faults. Latencies in the hundreds of milliseconds. What's going on! (This is for example a well-known "issue" with redis. Not really an issue but a technical necessity, but still... well-known enough to have its own FAQ page, and about 30k web sites writing about it.)

The nasty thing is that the reason why all these faults happen all of a sudden is not obvious (well... it is, but... not to you, at that time -- from your perspective there is no reason why they should happen). A second earlier, all was good, and now every memory access causes a fault. You didn't even touch anything! And to top it off with sugar, it's operating-system dependant...

And then, to maximize your joy, you are running on a Linux system with huge pages enabled, and you no longer understand a thing...

Sik_the_hedgehog
Sik_the_hedgehog

Don't forget about overcommiting. Enjoy processes dying out of nowhere without any warning or explanation.

As for speed, reminds me of a situation I had with memcmp. Apparently at least on some implementations it's optimized for comparing large chunks of data when I was using it to compare a few bytes instead - but not a fixed amount so the compiler can't just unroll it. And this was in a tight loop. Turns out that the overhead of preparing for a large comparison outweighted the benefit, to the point that simply adding a simple check for the first byte (since most comparisons would fail) would speed it up like crazy.

Just because the standard library is probably better optimized than what you can do doesn't necessarily mean it'll be best optimized for your actual case.

Don't pay much attention to "the hedgehog" in my nick, it's just because "Sik" was already taken =/ By the way, Sik is pronounced like seek, not like sick.
L. Spiro
L. Spiro
A coworker provided this extremely fast approximation.

void memcpy(void* dst, void*src, size_t size)
{
                if (size >= 1 && dst != src)
                {
                                *dst = *src;
                }

                // The rest can’t be that important
}
L. Spiro
I restore Nintendo 64 video-game OST’s into HD! https://www.youtube.com/channel/UCCtX_wedtZ5BoyQBXEhnVZw/playlists?view=1&sort=lad&flow=grid
xycsoscyx
xycsoscyx

Can't you just use a lookup table, like you do with the trigonometry functions?

Just create an array of RAM, then set the destination equal to ram[sourceAddress].

rip-off
rip-off

Depends on how much is good enough:

[source]

const int GOOD_ENOUGH_PERCENT = 95;

void *memapprox(void * destination, const void * source, size_t num) {

memcpy(destination, source, num * GOOD_ENOUGH_PERCENT / 100);

}

[/source]

fastcall22
fastcall22
Or approximate the bits copied:

const char GOOD_ENOUGH_MASK    = 0xF8;
const int  GOOD_ENOUGH_PERCENT = 90;

// uses less power than memcpy
void* memcpy_approx(void* dst, const void* src, size_t len) {
    char* pdst = (char*)dst;
    const char* psrc = (char*)src;
    const char* pend = psrc + (len * GOOD_ENOUGH_PERCENT / 100);

    while ( psrc < pend ) {
        *pdst++ = *psrc++ & GOOD_ENOUGH_MASK;
    }

    return dst;
};
Norman Barrows
Norman Barrows

how about:

for (a=count; a>0; a--, *(p1+count-a)=*p2+count-a));

it does a dec (sometimes faster), and pointer math to boot!

does it even compile?

Edit: Aw! Fastcall be me to the pointer math.

Norm Barrows Rockland Software Productions "Building PC games since 1989"</
21st Century Moose
21st Century Moose

50% accuracy!


for (int i = 0; i < len; i += 2)
    dst[i] = src[i];
Direct3D has need of instancing, but we do not. We have plenty of glVertexAttrib calls. 
Nypyren
Nypyren
Everyone knows that null values lead to access violations. So why not just prevent those pesky nulls from being copied in the first place?

void memcpy(void *dest, void *src, size_t size)
{
  strncpy((char*)dest, (const char*)src, size);
}
Hodgman
Hodgman

This one actually works... within a given assumption :wink:


void memcpy(void *dest, void *src, size_t size)
{
  assert(dest == src);
}
Daixiwen
Daixiwen

I'm surprised no one came with a solution using 3 or 4 boost template classes that would magically be optimized by the intelligent compiler.

Definition of a man-year: 730 people trying to finish the project before lunch
Juliean
Juliean

template<typename Type, size_t Num>
constexpr Type[Num] memcpy(Type[Num] src)
{
    return src;
}

Thats the modern C++11 variant. Better syntax (why pass in dest when we have return-values), and zero runtime overhead. And I'm not even sure if that even compiles and/or works, as an added bonus ;)

Topic Locked

This topic has been locked by a moderator. New replies are not allowed.

Sign in to reply to this topic.