Jump to content

  • Log In with Google      Sign In   
  • Create Account

new[] is flawed?


Old topic!
Guest, the last post of this topic is over 60 days old and at this point you may not reply in this topic. If you wish to continue this conversation start a new topic.

  • You cannot reply to this topic
38 replies to this topic

#1 Seabolt   Members   -  Reputation: 633

Like
0Likes
Like

Posted 03 October 2013 - 11:11 AM

Hey guys, I have a strange problem and I don't know how best to word this.

At my work we have a custom memory manager. It works just fine, but the man who wrote it says that new[] cannot be used because of underlying issues regarding alignment, he thinks. It's been awhile since he wrote this code and he doesn't quite remember.

But when I was writing code the other day, using new[], I found an issue where our memory manager would return a pointer to the overloaded new[] but the pointer I received from the actual new[] was four bytes off. Let me give you some pseudocode to better describe this.

void* operator new( int size )
{
      void* memory = m_MemoryManager->AllocateSize( size ); // Let's say the pointer was 4
      return memory; // Pointer is still 4
}

...

x* stuff = new x[ 4 ]; // x's pointer will now be 8.

The weird thing is that the pointer offsetting is happening when the stack is unwinding, some space between the actual function and the place where "stuff" is being allocated.

I imagine this mystery code is where the compiler actually invokes the constructor for each element of the array, but why would it alter the address?

Can someone explain like I'm five what is actually going on here?

(This is not in a multi-threaded environment, so nothing should be altering the pointer from underneath me.)


Perception is when one imagination clashes with another

Sponsor:

#2 ApochPiQ   Moderators   -  Reputation: 16391

Like
12Likes
Like

Posted 03 October 2013 - 11:17 AM

The standard actually specifies this IIRC; there is some overhead for array allocations which is commonly used for bookkeeping, such as stashing the length of the array, or other memory-management needs.

#3 Seabolt   Members   -  Reputation: 633

Like
0Likes
Like

Posted 03 October 2013 - 11:21 AM

Hmm I think I remember what you're talking about, somethinga bout knowing how many objects to call the destructor for. The strange thing was that it wouldn't do it on some pointers, but would on others. I just wanted to make sure there wasn't a way to solve this issue, because I really like having arrays of objects. It's so nice for cache coherency!


Perception is when one imagination clashes with another

#4 Brother Bob   Moderators   -  Reputation: 8575

Like
6Likes
Like

Posted 03 October 2013 - 11:44 AM

Could be that, for example, the length of the array is needed in order to know how many objects to call the destructor on when the memory is released. But if the objects are of primitive type or of a type with no/empty destructor, then no destructor call is necessary and thus no overhead is necessary to keep the length of the array. So different types may need different bookkeeping information.



#5 Seabolt   Members   -  Reputation: 633

Like
0Likes
Like

Posted 03 October 2013 - 11:49 AM

That would make sense. I'd have loved for some consistency so I could have accounted for it though...


Perception is when one imagination clashes with another

#6 Servant of the Lord   Crossbones+   -  Reputation: 20987

Like
2Likes
Like

Posted 03 October 2013 - 01:34 PM

If you are allocating 4 ints, and the pointer moves from, say, 0x04 to 0x08, doesn't this mean that your 'size' variable is also getting changed, to add 4 extra bytes?
So there's nothing you really need to take account for, since it is automatically handled.
 
Here's a quick test to be sure:

#include <iostream>
#include <cstdint>
using namespace std;

void* operator new( size_t size )
{
	std::cout << "The size 'new' is actually asking for: " << size << " bytes." << std::endl;
    void* memory = malloc( size ); // Let's say the pointer was 4
    
    std::cout << "Original address: " << memory << std::endl;
    
    return memory; // Pointer is still 4
}

class Object
{
public:
	Object() = default;
	virtual ~Object() = default; //Virtual, to give it a vtable, to ensure it's non-POD.
	
	int meow = 357;
};

struct POD
{
	uint64_t A;
	uint64_t B;
};

int main()
{
	const int NumObjectsToAllocate = 4;
	std::cout << "Sizeof 'Object': " << sizeof(Object) << std::endl;
	std::cout << "Allocating " << NumObjectsToAllocate
	          << " Objects should take " << (NumObjectsToAllocate * sizeof(Object))
	          << " bytes." << std::endl;
	
	Object *stuff = new Object[ NumObjectsToAllocate ];
	
	std::cout << "Resulting address: " << stuff << std::endl;
	
	delete[] stuff;
	
	std::cout << "---------------------------------------" << std::endl;
	
	const int NumOfFloatsToAllocate = 4;
	std::cout << "Sizeof 'float': " << sizeof(float) << std::endl;
	std::cout << "Allocating " << NumOfFloatsToAllocate
	          << " floats should take " << (NumOfFloatsToAllocate * sizeof(float))
	          << " bytes." << std::endl;
	
	float *floats = new float[ NumOfFloatsToAllocate ];
	
	std::cout << "Resulting address: " << floats << std::endl;
	
	delete[] floats;
	
	std::cout << "---------------------------------------" << std::endl;
	
	const int NumOfPodsToAllocate = 4;
	std::cout << "Sizeof 'Pod': " << sizeof(POD) << std::endl;
	std::cout << "Allocating " << NumOfPodsToAllocate
	          << " pods should take " << (NumOfPodsToAllocate * sizeof(POD))
	          << " bytes." << std::endl;
	
	POD *pods = new POD[ NumOfPodsToAllocate ];
	
	std::cout << "Resulting address: " << pods << std::endl;
	
	delete[] pods;
	
	return 0;
}

 
Results (with this compiler):
Sizeof 'Object': 8
Allocating 4 Objects should take 32 bytes.
The size 'new' is actually asking for: 36 bytes.
Original address: 0x9f54008
Resulting address: 0x9f5400c
---------------------------------------
Sizeof 'float': 4
Allocating 4 floats should take 16 bytes.
The size 'new' is actually asking for: 16 bytes.
Original address: 0x9f54030
Resulting address: 0x9f54030
---------------------------------------
Sizeof 'Pod': 16
Allocating 4 pods should take 64 bytes.
The size 'new' is actually asking for: 64 bytes.
Original address: 0x9f54048
Resulting address: 0x9f54048

 
So it's not just changing the pointer address, but it's also adding 4 bytes to the total size allocated. There's nothing you need to take into account.

I'm purely speculating here... but since (with this specific compiler) POD structs also don't require any extra bytes, I wonder if the 4 extra bytes for non-POD types is actually a pointer to the originally allocated type's destructor?

Imagine:

Base *objects = new Derived[10]; //All these are guaranteed to be the same type: Derived.
delete[] objects; //So they should all use the same destructor: Derived's destructor (if Base's destructor was virtual).

The compiler would know that 'objects' can be treated as type 'Base', but for destruction purposes might not realize that they are actually 'Derived', so maybe the first 4 bytes (sizeof a pointer-to-func on a 32 bit machine) are pointing at the correct destructor to call.

 

[/end-of-amature-speculation-from-someone-who-doesn't-know-assembly-or-the-inner-workings-of-compilers]

 

But regardless of what the compiler is using it for, it's taken care of for you, so there is nothing you need to manually take into account. Yes, it's allocating some extra bytes, but at the same time it's offsetting the pointer, and upon destruction it's destroying the correct number of bytes. There's no problem, unless you needed something to be guaranteed to be at a specific address in memory because of some esoteric hardware architecture - and in that really obscure, really unusual circumstance, then that's when you use malloc() directly.


It's perfectly fine to abbreviate my username to 'Servant' rather than copy+pasting it all the time.
All glory be to the Man at the right hand... On David's throne the King will reign, and the Government will rest upon His shoulders. All the earth will see the salvation of God.
Of Stranger Flames - [indie turn-based rpg set in a para-historical French colony] | Indie RPG development journal

[Fly with me on Twitter] [Google+] [My broken website]

[Need web hosting? I personally like A Small Orange]


#7 ApochPiQ   Moderators   -  Reputation: 16391

Like
2Likes
Like

Posted 03 October 2013 - 01:57 PM

A reasonable speculation, SotL, but you forget one critical detail: if the four bytes were indeed a destructor pointer, they would (A) need to be 8 bytes on 64-bit platforms and (B) obviate the need for virtual destructors when using arrays. which is definitely not the case.



#8 Paradigm Shifter   Crossbones+   -  Reputation: 5433

Like
1Likes
Like

Posted 03 October 2013 - 02:01 PM

Why don't they store the housekeeping data preceding the allocation then, making sure the returned pointer is aligned? Operator delete[] could find the housekeeping information based on the pointer passed to it, via the magic of subtraction.

 

Seems a no-brainer to me... (maybe the implementation was implemented before alignment became a major issue though, and it is retained for backwards compatibility).


"Most people think, great God will come from the sky, take away everything, and make everybody feel high" - Bob Marley

#9 SiCrane   Moderators   -  Reputation: 9667

Like
0Likes
Like

Posted 03 October 2013 - 02:06 PM


Why don't they store the housekeeping data preceding the allocation then, making sure the returned pointer is aligned? Operator delete[] could find the housekeeping information based on the pointer passed to it, via the magic of subtraction.

Because you don't know that the address there is writable.



#10 Paradigm Shifter   Crossbones+   -  Reputation: 5433

Like
2Likes
Like

Posted 03 October 2013 - 02:09 PM

The OS does though, unless you use a placement form, is that the reason then?


"Most people think, great God will come from the sky, take away everything, and make everybody feel high" - Bob Marley

#11 SiCrane   Moderators   -  Reputation: 9667

Like
2Likes
Like

Posted 03 October 2013 - 02:17 PM

I think you're making some very unsound assumptions about generalities of memory allocation schemes. Why would the C++ implementation be able to assume that the memory directly preceding that returned from an allocator be writable? If the underlying allocator grabs whole pages at a time from the OS that preceding memory could very well be a page with the write permission disabled.



#12 tonemgub   Members   -  Reputation: 1146

Like
0Likes
Like

Posted 03 October 2013 - 02:26 PM

Maybe it's ASLR? But I don't know if it's effects would be detectable in a debugger like in your case.

 

See this compiler option: http://msdn.microsoft.com/en-us/library/bb384887.aspx

 

And I think in Windows 7 ASLR is always used, even for programs compiled without that option?



#13 Paradigm Shifter   Crossbones+   -  Reputation: 5433

Like
3Likes
Like

Posted 03 October 2013 - 02:26 PM

The new[] implementation should ask for more memory than required (it already does, it asks for the housekeeping data amount extra), but it should ask for enough to make sure the address minus the housekeeping data size is aligned, then return the address which is correctly aligned, beyond the housekeeping data.

 

EDIT: I realise this method could require an entire (aligned size - housekeeping size) amount of memory to be wasted. What is probably needed is a general purpose allocator that can handle requests such as e.g. "give me x bytes at address of your choosing p, but I want the address (p+4) to be aligned on a 16 byte boundary").

 

EDIT2: Corrected the EDIT.


Edited by Paradigm Shifter, 03 October 2013 - 02:35 PM.

"Most people think, great God will come from the sky, take away everything, and make everybody feel high" - Bob Marley

#14 SiCrane   Moderators   -  Reputation: 9667

Like
4Likes
Like

Posted 03 October 2013 - 02:35 PM

Where does it say that existing C++ implementations aren't already doing that?



#15 Paradigm Shifter   Crossbones+   -  Reputation: 5433

Like
1Likes
Like

Posted 03 October 2013 - 02:37 PM

Evidence (maybe anecdotal!) from this thread... getting back unaligned requests for arrays from new[].

 

Aligned malloc usually only takes a size and an alignment restriction as well. It probably needs another constraint as an argument (i.e. the offset from the base address returned which needs to be aligned).


Edited by Paradigm Shifter, 03 October 2013 - 02:39 PM.

"Most people think, great God will come from the sky, take away everything, and make everybody feel high" - Bob Marley

#16 ApochPiQ   Moderators   -  Reputation: 16391

Like
0Likes
Like

Posted 03 October 2013 - 02:38 PM

Maybe it's ASLR? But I don't know if it's effects would be detectable in a debugger like in your case.
 
See this compiler option: http://msdn.microsoft.com/en-us/library/bb384887.aspx
 
And I think in Windows 7 ASLR is always used, even for programs compiled without that option?



ASLR has nothing to do with this. It's purely a function of memory obtained from the OS which is going to be a level lower than memory obtained from the language runtime (or a custom allocator).

#17 Brother Bob   Moderators   -  Reputation: 8575

Like
0Likes
Like

Posted 03 October 2013 - 02:40 PM

Evidence (maybe anecdotal!) from this thread... getting back unaligned requests for arrays from new[].

 

Aligned malloc usually only takes a size and an alignment restriction as well. It probably needs another constraint as an argument (i.e. the offset from the base address returned which needs to be aligned).

Nothing presented so far in this thread has shown an allocation not suitable for the data it was allocated for.



#18 Paradigm Shifter   Crossbones+   -  Reputation: 5433

Like
1Likes
Like

Posted 03 October 2013 - 02:41 PM

Yeah, that's why I added "maybe anecdotal" ;)


"Most people think, great God will come from the sky, take away everything, and make everybody feel high" - Bob Marley

#19 SiCrane   Moderators   -  Reputation: 9667

Like
1Likes
Like

Posted 03 October 2013 - 02:42 PM


Evidence from this thread... getting back unaligned requests for arrays from new[].

The only evidence in this thread is SotL's post and all the memory addresses are properly aligned for the types involved. That's not even anecdotal evidence since what you claim to be seeing hasn't actually shown up.



#20 Paradigm Shifter   Crossbones+   -  Reputation: 5433

Like
0Likes
Like

Posted 03 October 2013 - 02:47 PM

 

At my work we have a custom memory manager. It works just fine, but the man who wrote it says that new[] cannot be used because of underlying issues regarding alignment, he thinks. It's been awhile since he wrote this code and he doesn't quite remember.

 

There ya go... anecdotal evidence right there in the OP! His colleague thinks he had an anecdote but maybe he didn't quite remember!

 

I'm not trying to start an argument... I just said how I think it should work (in an ideal world), you jumped on it, then I said what it should do implementation wise to ensure it can cope, and you said how do I know it doesn't do it anyway! Time for more beers I think!!!

 

EDIT: Lulz I've annoyed someone ;) As I said, not trying to start an argument...


Edited by Paradigm Shifter, 03 October 2013 - 02:54 PM.

"Most people think, great God will come from the sky, take away everything, and make everybody feel high" - Bob Marley




Old topic!
Guest, the last post of this topic is over 60 days old and at this point you may not reply in this topic. If you wish to continue this conversation start a new topic.



PARTNERS