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

Assignment vs Memcpy

Started by asdfg__12 Jul 3, 2007 at 4:50 PM 9 replies 10.1k views
Original Post
asdfg__12
asdfg__12
Hello, i 'd like to share a performance observation i made about the following code:

#include <iostream>

#define UInt8 unsigned char
#define UInt16 unsigned short
#define UInt32 unsigned int
#define UInt64 unsigned long long

inline UInt64 CPUClock()
{
	UInt32 lo, hi;
	__asm__ __volatile__ ("rdtsc" : "=a" (lo), "=d" (hi));
	return (UInt64)hi << 32 | lo;
};

struct Test
{
	UInt8 byte0;
	UInt8 byte1;
	UInt8 byte2;
	UInt8 byte3;
	
	Test(){};
	
	Test(UInt8 a_byte0, UInt8 a_byte1, UInt8 a_byte2, UInt8 a_byte3):
	byte0(a_byte0), byte1(a_byte1), byte2(a_byte2), byte3(a_byte3){};
	
	/*inline Test &operator=(Test &a_test)
	{
		memcpy((void*)this, (void*)(&a_test), sizeof(Test));
		return *this;
	};*/
};

#define N 2048576

int main()
{
	int size = sizeof(Test);
	
	Test *src = new Test[N];
	Test *dst = new Test[N];
	
	UInt64 start;
	UInt64 end;

	start = CPUClock();
	
	for(int i = 0; i < 1000; i++)
	{
		for(int j = 0; j < N; j++)
		{
			dst[j] = src[j];
		};
	};
	
	end = CPUClock();
	
	std::cout << "Time:\t" << ((float)(end - start)) / 3010500.0 << std::endl;
	
	return 0;
};



when i use the default assignment operator of the Test struct, the execution time is around 6600 milliseconds, but when i overload the assignment operator use memcpy with the size of the block to copy being known at compile time, the same program executes at around 3800 milliseconds. I get the same speed improvement if i replace this:

UInt8 byte0;
UInt8 byte1;
UInt8 byte2;
UInt8 byte3;


with this:

union
{
     struct
     {
          UInt8 byte0;
          UInt8 byte1;
          UInt8 byte2;
          UInt8 byte3;
     };
};



I 'm using g++ 4.1.2 with the -O6 option. The default assignment probably does 4 * 1 byte copies instead of doing 1 * 32-bit like memcpy probably does. But this optimization is done if i use the default assignment operator and just enclose the data member declarations in a union{struct{ }} block. I haven't tested this on visual studio because it ' s currently not installed on my pc. I would just like to read some comments on this, for example is my test invalid for some reason, does this happen in MSVC too? If i replace one or two members with 16-bit ints, the speed gain is much smaller, but it exists, and maybe in some cases it would be needed.
ToohrVyk
ToohrVyk
Very interesting, and it does make sense: the presence of a constructor marks the object as non-POD, so the compiler generates a member-wise assignment operator, which in turn incurs an alignment property.

I'd be curious to know if removing the default constructor or the union around the inner struct have any effect on the performance. I will try to profile this on MSVC when I'm at work later this morning.

As for your test, a variance information would be useful. Run the test a few hundred times (perhaps with a slightly smaller number of copies objects), and compute the average and variance of the results, so we know in what ranges the two durations are. Also, doing the test inside the cache (with a small N but a large number of iteratins) would be interesting as well, in case memcpy optimizes something around there.
asdfg__12
asdfg__12
Okay, i tried it in msvc 2005 too, and the results seem to be the same in all cases(with small or large N, default assignment/memcpy, compiler generated/handwritten default constructor, with or without the unnamed union/struct). It just seems g++ does some extra optimizations with it's memcpy, maybe even doing an optimized memcpy when i place the unnamed union/struct block. Unfortuately i don't have time to search into it more now, maybe i 'll do some googling later today.
Bregma
Bregma
Quote:
Original post by ToohrVyk
Very interesting, and it does make sense: the presence of a constructor marks the object as non-POD, so the compiler generates a member-wise assignment operator, which in turn incurs an alignment property.


The presence of a constructor does not render the class a non-POD type. The fact that the compiler is likely to align members on a word boundary is more apt to be the cause of the difference. Try invoking the compiler directive to pack the structure (#pragma pack for MSVC, __attribute__((__packed__)) for GCC).

--smw
Stephen M. Webb
Professional Free Software Developer
abdulla
abdulla
On a side note, regular GCC only goes up to -O3. Only special version of it go up to -O4. So the -O6 is a bit overkill.
Iftah
Iftah
odd, I don't have the same problem.

I compiled the code you posted using "gcc version 3.4.4 20050721 (Red Hat 3.4.4-2)"

without optimization : 11792.1
with -O3 (or O6): 4275.9

the union-struct had no effect.

Iftah.
asdfg__12
asdfg__12
It might be the different compiler versions. I 'm using ubuntu and g++ 4.1.2. Did you use the overloaded assignment operator as well?
Iftah
Iftah
yea, I the compiler versions differ, but it seems you are using a newer version?
I guess newer isn't always better.

I checked with the overloaded version-
with optimization -O3 fast as usual (around 4.2 seconds),
without optimization super slow (I think it was around 75 seconds).
inline does wonders [smile]
jpetrie
jpetrie
Although it probably will not affect your benchmarking wildly, your use of rdtsc doesn't follow the recommended conventions for using the instruction for performance monitoring.

EDIT: This article is newer, and might contain more updated considerations (the original document is from 1998).
asdfg__12
asdfg__12
Thx for mentioning this article, i will definately read it :)
CrazyCdn
CrazyCdn
More articles on timers, sadly they're not so simple anymore:

http://www.gamedev.net/reference/programming/features/timing/
http://www.gamedev.net/community/forums/topic.asp?topic_id=451305 (links to the above)
http://blogs.msdn.com/oldnewthing/archive/2005/09/02/459952.aspx
http://www.gamedev.net/community/forums/topic.asp?topic_id=420586

And what I think is the best so far:
http://www.geisswerks.com/ryan/FAQS/timing.html

I ended up writing a timer class in our engine that lets you, at compile time, change which timer your using. From timeGetTime() using timeBegin/EndPeriod(1) to RDTSC and finally QPC/QPF. The default right now is QPC/QPF, it works fine on my Intel Core2Duo. But 1ms resolution with timeGetTime() is more then enough for the engine honestly. One nice thing I added was a check to see if the person wanted to use QPC/QPF and if they compiled with it they can still disable it in a config file for AMD dual core users if there is still an issue.
"Those who would give up essential liberty to purchase a little temporary safety deserve neither liberty nor safety." --Benjamin Franklin

Topic Locked

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

Sign in to reply to this topic.