
On C Linked Lists (Profiling and Optimizing) - djcapelis
http://rusty.ozlabs.org/?p=168
======
tptacek
Generally, if you care about performance, you're using a collection that
doesn't malloc individual nodes, and allows random access without crawling
pointer chains and tearing up your cache.

Instead of wasting more energy on improving the (overused) linked list, write
a little resizable array library that doubles size on each fill-up. It's easy
and much faster.

~~~
neild
_Instead of wasting more energy on improving the (overused) linked list, write
a little resizable array library that doubles size on each fill-up. It's easy
and much faster._

Don't do this.

Doubling the size of your allocation whenever you expand can easily lead to
large amounts of wasted storage in common scenarios. "Yes," you may say. "But
memory is cheap." Which it is at the small scale, but when you're allocating
1GB to store a 512MB array you have a problem.

A vastly better approach is to use some form of adaptive rescaling which
avoids excessive overcommits.

Here, for example, is the code (and explanatory comment) Python uses to
determine the new allocation size for an expanding list:

    
    
            /* This over-allocates proportional to the list size, making room
             * for additional growth.  The over-allocation is mild, but is
             * enough to give linear-time amortized behavior over a long
             * sequence of appends() in the presence of a poorly-performing
             * system realloc().
             * The growth pattern is:  0, 4, 8, 16, 25, 35, 46, 58, 72, 88, ...
             */
            new_allocated = (newsize >> 3) + (newsize < 9 ? 3 : 6) + newsize;
            if (newsize == 0)
                    new_allocated = 0;
    

Note that you will also want to avoid using the same calculation for expanding
and contracting the allocation size, to avoid thrashing allocations when the
list size varies around the pivot point. (i.e., add one element, triggering an
expansion; remove the element, triggering a contraction; repeat.)

~~~
tptacek
This is a great comment and I guess it's good advice, but doubling vectors
have never caused a problem for me, I profile, and I've had them dealing with
data sets like "each side of every TCP connection traversing 1/8th of an
entire tier 1 ISP backbone".

I'll steal the Python trick, because it's neat, but I don't think worrying
about your resize function is a good reason to procrastinate moving away from
linked lists, which are mostly evil.

------
ced
How does deletion work in ring-style lists when there's only one element in
the list? Wouldn't that break a branchless implementation? How is the empty
ring represented, anyway?

As for his test purportedly showing that A is better than B on Linux... It's
only right if you assume that Linux can only use one type of list in all
cases.

~~~
rwmj
An empty ring never needs to be presented. The point about the Linux list is
that it lives inside another struct. If there are no instances of that struct,
then there is no need to represent an empty list at all!

~~~
tedunangst
I think you're confused. The list head exists outside of the structs that are
in the list. The list links (next and prev) live in the struct, but not the
head.

To answer the original question, you decide a list is empty when prev == next.

~~~
ced
But isn't that true if the list has one element? Then A->A, thus prev==next,
but the list still isn't empty.

And if the head lives outside of the list proper, then you would need special-
case code to handle deletion of the first element in the list (since the head
contains a pointer to it). It wouldn't be branchless.

~~~
tedunangst
There is a fake element that is the head. It doesn't count as part of the
list, but it's always in the list. With one element, the list is

    
    
        H->next = A, A->next = H
        H->prev = A, A->prev = H

~~~
ced
Oh, that's a clever solution. Thanks.

