views:

210

answers:

8

So the title is somewhat misleading... I'll keep this simple: I'm comparing these two data structures:

  1. An array, whereby it starts at size 1, and for each subsequent addition, there is a realloc() call to expand the memory, and then append the new (malloced) element to the n-1 position.
  2. A linked list, whereby I keep track of the head, tail, and size. And addition involves mallocing for a new element and updating the tail pointer and size.

Don't worry about any of the other details of these data structures. This is the only functionality I'm concerned with for this testing.

In theory, the LL should be performing better. However, they're near identical in time tests involving 10, 100, 1000... up to 5,000,000 elements.

My gut feeling is that the heap is large. I think the data segment defaults to 10 MB on Redhat? I could be wrong. Anyway, realloc() is first checking to see if space is available at the end of the already-allocated contiguous memory location (0-[n-1]). If the n-th position is available, there is not a relocation of the elements. Instead, realloc() just reserves the old space + the immediately following space. I'm having a hard time finding evidence of this, and I'm having a harder time proving that this array should, in practice, perform worse than the LL.

Here is some further analysis, after reading posts below:

[Update #1] I've modified the code to have a separate list that mallocs memory every 50th iteration for both the LL and the Array. For 1 million additions to the array, there are almost consistently 18 moves. There's no concept of moving for the LL. I've done a time comparison, they're still nearly identical. Here's some output for 10 million additions:

(Array)
time ./a.out a 10,000,000
real    0m31.266s
user    0m4.482s
sys     0m1.493s
(LL)
time ./a.out l 10,000,000
real    0m31.057s
user    0m4.696s
sys     0m1.297s

I would expect the times to be drastically different with 18 moves. The array addition is requiring 1 more assignment and 1 more comparison to get and check the return value of realloc to ensure a move occurred.

[Update #2] I ran an ltrace on the testing that I posted above, and I think this is an interesting result... It looks like realloc (or some memory manager) is preemptively moving the array to larger contiguous locations based on the current size. For 500 iterations, a memory move was triggered on iterations: 1, 2, 4, 7, 11, 18, 28, 43, 66, 101, 154, 235, 358 Which is pretty close to a summation sequence. I find this to be pretty interesting - thought I'd post it.

A: 

(Updated.) As others have noted, if there are no other allocations in between reallocs, then no copying is needed. Also as others have noted, the risk of memory copying lessens (but also its impact of course) for very small blocks, smaller than a page.

Also, if all you ever do in your test is to allocate new memory space, I am not very surprised you see little difference, since the syscalls to allocate memory are probably taking most of the time.

Instead, choose your data structures depending on how you want to actually use them. A framebuffer is for instance probably best represented by a contiguous array.

A linked list is probably better if you have to reorganise or sort data within the structure quickly.

Then these operations will be more or less efficient depending on what you want to do.

(Thanks for the comments below, I was initially confused myself about how these things work.)

Amigable Clark Kant
Good answer; could you provide some citations?
San Jacinto
I don't know why you think over commit means no copying. There's still only one virtual address space per process, and if the memory can't be expanded contiguously, it has to be copied.
Matthew Flaschen
realloc will very often copy the data in real scenarios, simply because you might allocate other objects inbetween, and the address spaces are no longer continuous. In a test scenario where you only realloc an array, and do no other mallocs, the memory piece might just be extended. This has nothing to do with overcommit though.
nos
@Matthew, I guess you are right, about Linux. If other pointers already occupy space where your to be extended, (to be realloced) pointer is, then yes, data has to be copied.
Amigable Clark Kant
It's true of OpenBSD too. The only point of a guard page is to detect certain invalid memory accesses. `realloc` still copies when needed.
Matthew Flaschen
Yes @Matthew, you are right.
Amigable Clark Kant
A: 

What's the basis of your theory that the linked list should perform better for insertions at the end? I would not expect it to, for exactly the reason you stated. realloc will only copy when it has to to maintain contiguity; in other cases it may have to combine free chunks and/or increase the chunk size.

However, every linked list node requires fresh allocation and (assuming double linked list) two writes. If you want evidence of how realloc works, you can just compare the pointer before and after realloc. You should find that it usually doesn't change.

I suspect that since you're calling realloc for every element (obviously not wise in production), the realloc/malloc call itself is the biggest bottleneck for both tests, even though realloc often doesn't provide a new pointer.

Also, you're confusing the heap and data segment. The heap is where malloced memory lives. The data segment is for global and static variables.

Matthew Flaschen
"realloc will only copy when it has to, while every linked list node requires allocation and (assuming double linked list) two writes." - You're forgetting that realloc is still going to allocate space for the new entry. It must still check the heap and reserve the n+1 space after the array (assuming a move doesn't occur).
Jmoney38
@Jmoney, I've clarified. You're correct that it may have to update the bookkeeping information. This isn't always true, though. E.g. if you `malloc` 10 bytes, it might give you a 16-byte chunk. If you `realloc` to 11 bytes, it doesn't need to do anything.
Matthew Flaschen
+3  A: 

So you're testing how quickly you can expand an array verses a linked list?

In both cases you're calling a memory allocation function. Generally memory allocation functions grab a chunk of memory (perhaps a page) from the operating system, then divide that up into smaller pieces as required by your application.

The other assumption is that, from time to time, realloc() will spit the dummy and allocate a large chunk of memory elsewhere because it could not get contiguous chunks within the currently allocated page. If you're not making any other calls to memory allocation functions in between your list expand then this won't happen. And perhaps your operating system's use of virtual memory means that your program heap is expanding contiguously regardless of where the physical pages are coming from. In which case the performance will be identical to a bunch of malloc() calls.

Expect performance to change where you mix up malloc() and realloc() calls.

PP
"The other assumption is that, from time to time, realloc() will spit the dummy and allocate a large chunk of memory elsewhere because it could not get contiguous chunks within the currently allocated page" - This is exactly what I'm expecting to cause a degradation of performance, but it's not happening.
Jmoney38
+7  A: 

You're right, realloc will just increase the size of the allocated block unless it is prevented from doing so. In a real world scenario you will most likely have other objects allocated on the heap in between subsequent additions to the list? In that case realloc will have to allocate a completely new chunk of memory and copy the elements already in the list.

Try allocating another object on the heap using malloc for every ten insertions or so, and see if they still perform the same.

harald
+1, this will be a good test case. Testing whether reallocating occurs is also quite easy, compare the new pointer returned from realloc with the old pointer.
nos
Updated the post...
Jmoney38
A: 

Will the compiler output be much different in these two cases?

dwarFish
A: 

Assuming your linked list is a pointer to the first element, if you want to add an element to the end, you must first walk the list. This is an O(n) operation.

Assuming realloc has to move the array to a new location, it must traverse the array to copy it. This is an O(n) operation.

In terms of complexity, both operations are equal. However, as others have pointed out, realloc may be avoiding relocating the array, in which case adding the element to the array is O(1). Others have also pointed out that the vast majority of your program's time is probably spent in malloc/realloc, which both implementations call once per addition.

Finally, another reason the array is probably faster is cache coherency and the generally high performance of linear copies. Jumping around to erratic addresses with significant gaps between them (both the larger elements and the malloc bookkeeping) is not usually as fast as doing a bulk copy of the same volume of data.

R..
Any reasonable linked list implementation will maintain a pointer to both ends of the list so you don't need to walk the whole length of it to add to the tail.
JeremyP
"A linked list, whereby I keep track of the head, tail, and size."
Jmoney38
A: 

This is not a real life situation. Presumably, in real life, you are interested in looking at or even removing items from your data structures as well as adding them.

If you allow removal, but only from the head, the linked list becomes better than the array because removing an item is trivial and, if instead of freeing the removed item, you put it on a free list to be recycled, you can eliminate a lot of the mallocs needed when you add items to the list.

On the other had, if you need random access to the structure, clearly an array beats the linked list.

JeremyP
+1  A: 

The performance of an array-based solution expanded with realloc() will depend on your strategy for creating more space.

If you increase the amount of space by adding a fixed amount of storage on each re-allocation, you'll end up with an expansion that, on average, depends on the number of elements you have stored in the array. This is on the assumption that realloc will need to (occasionally) allocate space elsewhere and copy the contents, rather than just expanding the existing allocation.

If you increase the amount of space by adding a proportion of your current number of elements (doubling is pretty standard), you'll end up with an expansion that, on average, takes constant time.

Vatine