Showing posts with label sorting. Show all posts
Showing posts with label sorting. Show all posts

Sunday, September 19, 2010


Optimizing your file dialog



Recently, someone asked me why is it that the custom file dialogs in some programs I wrote are able to load a file list so much faster than the so called "native file dialogs" on those operating systems. The answer is that I optimized many areas of loading a file list, and displaying it. Today we'll look at some optimizations that can be performed, instead of using the most obvious approaches to most of the process.


  1. Know your system calls for reading directory entries



    You'll want to create some sort of library around the native directory functions in order to use the most efficient path on each operating system. Operating systems vary widely in what they they provide.

    Sometimes you have multiple APIs, or a single API with many options you can turn on and off. Unless the provided API is in itself doing everything as efficiently as possible (such as efficiently returning the file names in a sorted matter), you'll want to use the lowest level API, or turn off all options it provides.

    If bidirectional or scanning options are provided, turn them off, as you'll want forward only. The overhead for some of these features can significantly slow down the process of obtaining a file list. Same goes for anything else that adds overhead.

    Be aware of what information is provided other than file names. Some OSs will return a structure which may also contain file size or file permissions alongside each file. If you're filtering by any of these, use the information directly there, instead of using an additional call to stat() or similar.


    In addition to the above, if you need to filter by file size or file permissions, and the API does not provide that information directly, avoid the need for concatenating strings to get your information. A call to stat() would require concatenating a file name onto the directory name if it's not in the current directory. If your OS provides for it, use fstatat() or similar instead (see "OpenGroup Extended API Set Part 2").

  2. Process file names in an optimized manner


    Once you start getting back file name(s), before anything, determine the length of a file name, as all other operations will need to know the length. Pretty much every operating system / file system limits the length of a file name to 255 bytes, the length of that can fit in a single byte. Compute the length (if need be) and keep reusing this cache.

    If you want to do extension filtering, now is the time to do so. Do not use strrchr(), or anything of the sort to find where the extension begins (searching for a period). Functions like strrchr() will first travel to the end then look back, effectively being O(2N). Since you already know the length of the string, use an O(N) function to look back from the end. Perhaps even using one which can scan multiple bytes (whole CPU words) at a time.

    For extension filtering, the most common method is generally one that breaks down to a series of compares. Put a little more thought into your algorithm usage! Either sort your list of extensions and binary search it for a match, or use perfect hashing. In either case, make sure the sorting or hashing phase is performed just once.

    For copying the files names you want elsewhere, use memcpy() instead of strcpy() and similar. Don't forget to pass around the length for other uses too.


  3. Use efficient data structures efficiently


    The next question is how to keep track of your list of files. The most two common ideas are to use a vector and just keep adding to it, or use a linked list. Both those solutions are wrong.

    A vector while great in theory, has some performance and memory issues. When a vector can no longer contain what is being added to it, it doubles in size. The doubling in size can cause up to 99% more allocation than is actually needed during the growing process, which also comes with a lot of copying around. This may suit many workloads, but won't always suit the file list workload well.

    A linked list on the other hand is one of the worst possible data structures you can use. Every entry has 100-200% extra overhead for pointers, and often means many series of allocations. Many small allocations leads to performance issues, and a lot of memory fragmentation.

    The solution I would recommend is either a tweaked vector, or a combination of a vector plus linked list. Tweaking a vector starts off with reserving a set amount of space up front. Have your vector by default start with room for say 512 entries, and only then grow as needed. When you see your vector is full, instead of doubling in size, reserve another 512 entries. Smaller requests are more likely to be fulfilled for a realloc() in the same location, than a doubling in size. Very large directories beyond this point also becomes increasingly rarer at each new milestone. On top of all this, have your program remember how many files were in each of the past 10 or so directories it looked in (with filtering options of course). Then when loading a directory you recently loaded in the past, you can allocate an amount which is usually just what you need, or only needs one more chunk allocated onto it. Keep in mind that most users will keep using the same few directories over and over again with each application.

    The combination of the vector and linked list would instead build a structure out of the two ideas combined. Create your array of 512 entries, along with a fill pointer and and linked list pointers. When your array is full, allocate another block of this structure as the next chain in the linked list. This ensures no need to ever reallocate, or copy data around during the growing phase. Of course keep a global pointer to the current link being filled, instead of iterating to it each time you add a new entry.

    Which of these two options you use depends on how you want to sort and display your files. Each data structure has pros and cons for different use cases. Before we get into that, know how to store your file names themselves.

    The above description was for the pointers to the arrays holding the file names, the file names should NOT have a separate block of allocated memory for each. That's inefficient. Instead allocate large chunks of memory, say 16KB each. Store your file names in this memory pool. Forget the silly trailing null if you can, store the length before the name itself. The two data structures explained above for containing lists of files will point into these pools. If a pool can't contain another entry you're about to add, allocate another 16KB chunk, point to it from the previous one, and start allocating in this new one. If the need for a new one was for a rare overly large file, say 230 bytes, when 200 bytes were free, you can implement an algorithm to save some overhead by seeing if the next couple of files fit into the end of any previous pools. The pools in general will probably waste less than your OSs malloc() for small file names, and you'll certainly get better performance. Deallocation will also be a quick (backwards) traversal for large chunks instead of many small ones.

  4. Sort efficiently


    The first part of sorting is comparison. strcmp()/strncmp() is not the function to use. You want something based on memcmp() as that works faster thanks to having the length of the file name already cached. Of course that's only if you want standard sorting.

    If you want case insensitive sorting, other methods will have to be used. If you're only dealing with ASCII file names, and you're not worried about file names containing control characters or some symbols end up being of equal value, you can compare 4 bytes at a time or'ing each with 0x20202020 (0x2020202020202020 for 8 bytes at a time). You're probably out of luck otherwise for case insensitive, unless someone knows of any good tricks to use.

    If you need "natural sorting", where numbers are compared to each other, so file1 precedes file15, and file2 follows file1 not file15, there's a few implementations floating around online and most are just copies of one another. Sadly, the most popular one I see, which is present in two operating systems, and the native implementation in a particular programming language, happens to incorrectly handle numbers past a certain magnitude (did anyone even test the implementations or just accept them at face value?), and has a worse case of O(3N). So beware of what you use or base your code off of.

    I'd recommend something along these lines:

    int strnatbase(const char *s1, const char *s2)
    {
    for (; *s1; ++s1, ++s2) //No need to check s2, as we compare it to s1, and it won't equal if it's null
    {
    if (isdigit(*s1) && isdigit(*s2)) //If they're both digits, use this special handling
    {
    while (*s1 == '0') { ++s1; } while (*s2 == '0') { ++s2; } //First skip leading zeros

    register bool d1 = isdigit(*s1), d2 = isdigit(*s2); //Are we still in a run of digits for both strings?
    if (d1 != d2) { return(d1-d2); } //If for only one of them, return that as the difference

    for (; d1 && (*s1 == *s2); ++s2) { d1 = isdigit(*++s1); } //Keep going while we have matching digits

    d2 = isdigit(*s2); //If we broke the above loop because a single string ran out of digits
    if (d1 != d2) { return(d1-d2); } //Return that as the difference

    for (const char *p1 = s1, *p2 = s2; d1; ) //Otherwise, difference in the digits themselves, clarify magnitude
    {
    d1 = isdigit(*++p1); d2 = isdigit(*++p2); //First string to run out of digits first
    if (d1 != d2) { return(d1-d2); } //Will lose right here
    } //If loop breaks, both strings are out of digits, the difference found above will be handled below
    }
    if (NOT_EQUAL(*s1, *s2)) { break; } //In all other cases, fall through with difference in current position - if any
    }
    return(*s1-*s2);
    }


    Modify as necessary for unicode, and replace NOT_EQUAL() with your function (or macro) of choice for comparing two letters (accounting for case differences if you want).

    As for the sort functions themselves, it will depend on which data structure you chose. You'll probably want Quicksort for the vector. Make sure your Quicksort sorts the pointers to the data and not the data itself! Same for any sorting algorithm sorting this. Swapping pointers is quick and easy. For the latter structure, you might want Merge sort, or perhaps Smoothsort. What you choose can depend on several factors.

    You may or may not want to utilize threading. Depending on how you filter, the OS may be giving you mostly sorted file names which can also play a role in the algorithm you choose. In the case of the linked lists of arrays, you might want to sort each array with one algorithm, then sort the whole. If you're utilizing threading, you can begin sorting the array(s) as you're still obtaining files from the operating system, as disk reads are slower than memory reads (when the current directory contents is not (yet) cached by the OS).

  5. Display directly


    This point is rather simple. Have your file dialog display entries directly from whichever data structure you chose, without copying anything around. Know that most existing file dialog routines provided by various libraries will not be compatible with the data structures described above. So roll your own, or choose structures which can be passed directly to your library.

    Another possibility is to copy entries as they fit in the dialog window. The dialog is unable to show an infinite amount of entries at once, so convert from your structure to its structure just for the current files being viewed. Update as scrolling is performed.

  6. Search efficiently


    Many file dialogs provide the option to type in some characters, and the file dialog jumps to the first file that begins with those characters. Since your file dialog is sorted, use binary search. Some people don't realize this, but binary search can find more than just an exact match, binary search can also be used to find the transition from entries "less than" to "first equal match" (in other words, binary search can also be used to find the transition between entries before the first match and the first match itself). Use this to find the first matching file in O(log(N)).



This basically wraps up this discussion on how to optimize file dialogs. I once was annoyed that loading up a directory with 20,000 files in it on a particular OS with a particular file browser took a good torturesome 20-30 seconds. Now using my own, I can load a directory with 100,000 files in 1-2 seconds on the same setup.

Thoughts, improvements, complaints, and every other kind of comment is welcome. Be sure to comment if you take of these ideas to optimize your library or application. I'd love to hear about it.

Tuesday, September 9, 2008


Binary Trees - Does it matter?



Ever programmed a Binary Tree before? If you took computer science classes past an introductory level, you probably studied the theory before. But to what extent do decisions matter?

To review for those of you who don't quite recall what a binary tree is. You have a series of nodes, each node contains data, some or all of which is the key, up to two pointers to children nodes, and possibly other meta data. The most popular type of binary tree is the Binary Search Tree, where each node conceptually has nodes with lesser keys descending on the left, and nodes with greater keys descending on the right.



How to implement a left and right can be done with two node pointers called left and right, lesser and greater, an array of two node pointers, or any other method which makes sense to you.

All operations begin with the topmost node in the figure above, known as the root, head, or top node of the tree. To find a node with a particular key, one compares it against the current node, if it found it good, otherwise, if it's less, take the left route, otherwise the right. If there is no path left or right as requested, since there is only a null pointer there, then a node with a matching key is not in the tree. At each node reached, the same algorithm is applied. In this instance, a Binary Tree is very similar to Binary Search, each check effectively cuts the data in half, leading to very efficient searches.

Adding data to a tree is very much the same. The data is encapsulated in a node, which again goes down the tree finding where the data should belong, in this case, once a null pointer is encountered while trying to traverse the tree further, it is replaced by a pointer to the new node.

Deleting data can be a bit trickier. If a node has no children, deleting a node is quite straight forward. If the node only has a path left or right, one simply cuts out the node being deleted, and makes the pointer once pointing to it now point to the child. However, for removing a node which has two children descending from it, there's a number of methods that can be applied.

The simplest method is to make one child a replacement, and add the other child onto the child replacing, meaning add the left node to the right or the right to the left.

Look what happens when deleting the root node, which in this case has two children.

















The node with a key of 50 is deleted which had two children, 25 and 100. Either 25 becomes the new root, with 100 as the rightmost of 25's descendants, or 100 becomes the new root, with 25 as the leftmost of 100's descendants.

The only problem with this algorithm is that it can quickly make the tree become unbalanced, degrading into a linked list. A tree should be as close to balanced as possible, so search or addition can be done with cutting half the tree out per each node looked at. An unbalanced tree would make each operation generally consist of looking at most of the nodes in the tree, which is inefficient.

Therefore, the generally proposed algorithm is to find the next or previous node by key, and move it to replace the node being deleted. First lest us understand tree order.

A binary tree can be processed in direct order using the following algorithm.

function process_node(Node *node)
{
if (node->has_left()) { process_node(node->left()); }
process_node_data(node->data());
if (node->has_right()) { process_node(node->right()); }
}

Which is plain English means handle left, then current, then right, in a recursive fashion. Because a Binary Tree can be processed in this fashion, in addition to being a nice data structure, it can also be used as the basis of a sorting algorithm. All data is encapsulated in a node and is added to the tree, then it is traversed in order as just described, placing the data back into the original data structure, and then the tree is destroyed.

Now understanding this, the node prior to any particular node in regards to its key is always one left, and then rightmost, and the node after a particular key is one right, and then left most.

And the delete:


















50 is deleted, and the previous or next one takes its place. In this case, 35 being the previous, and 75 being the next one.

Now to get to the meat of the article, does it matter? Does what matter?

First off, we didn't at all mention how to handle nodes which have equivalent keys. Back when I was in college learning computer science, we were told to just throw it down the right or the left, but be consistent about it. Also, when it comes to deleting, take the previous or the next, but again, it doesn't matter, as long as one was consistent about it. Or perhaps in this latter case, alternate sides per delete.

But the question we must ask is: "Does it matter?"

And the answer which they at the very least didn't teach me when I was in school, yes, it does matter!

When one sorts, one can do a stable or an unstable sort. Stable means that if two elements had equivalent keys, when sorting, the order of those nodes would be preserved. Meaning if we had: "50 C, 45 A, 23 C, 50 B, 75 D, 50 A", and the numbers would make up the key, but not the letters, then it would sort to: "23 C, 45 A, 50 C, 50 B, 50 A, 75 D", keeping the order of C,B,A among the elements with a key of 50. Unstable would mean that the relative order of those 50s is not guaranteed to be kept when sorting.

A binary tree can only be used as a stable sort if equal data is always placed on the right side. This is because the order of succession is left, current, right, and if it was placed on the left, it would mean the node added later came before the current node.

What not to do:


However, just putting equal data on the right isn't the end of the matter yet. Most good Binary Trees, such as a Red & Black Tree will implement some kind of self balancing system, so areas which are starting to become linked lists rotate to become balanced trees.

Looks like a linked list:


As can be seen in the diagrams, the middle node of the three becomes the new parent of the section with the first node becoming the left. This still follows the in order rules of left, center, right, because the one of the left is indeed the first node. And rotations like this, would also preserve numbering orders of the keys (otherwise a Red & Black Tree or another self balancing tree would never employ them).

Now adding another 50 on our proposed case still works with our initially stated algorithm, we traverse down till we reach our area of 50s, then place it to the right of the right most one. However, we have to slightly rewrite our find a node which matches a key algorithm. If we want our tree to always return the first node which matches a key, or in the case of deletion, delete the first node which matches a key, one can't just search until one finds a matching node, since it in fact may be the second or third node if rotations took place. Rather, one has check to see if the left contains the same value, and if so, go left most across matching keys, and return the deepest one found there.

Now after reviewing these revised rules, is your binary tree written properly? Furthermore, have you ever even seen an implementation which when searching checks left most for matching keys?

Oh, and before we leave, what about deleting, does it matter if the prior or the next node is chosen? The other night, a student of mine asked me that, and presented to me a case which we both agreed proved that to properly delete and keep a stable sort, one has to use the next node, and not the prior one to properly preserve the key order. Although, we were both tired at the time, and now when trying to recreate the case I can't seem to do it. So I do hope I was in error the other night, and am not leaving out a crucial point. If you happen to know of a case where it matters, please describe it in the comments.

Wednesday, March 28, 2007


Quicksort



Sorting is something most programs have to do on some occasion. Quicksort is accepted as the best in-practice general purpose sorting algorithm available today. Standard C offers a built in Quicksort under the function name qsort(), with the following prototype:


void qsort(void *base, size_t nmemb, size_t size, int(*compar)(const void *, const void *));


Now while it may be built in, each C library implements it differently. Also, it's designed to work with any kind of type/structure, however one can generally optimize it a bit more for the kind that they're dealing with. Since sorting is important, and can take a while with many items, one generally wants their sorting to be optimized as much as possible.

I've looked at several implementations for various C libraries out there, however they all have one thing in common - messy. One I looked at didn't even implement Quicksort, but a Shakersort.

I'm wondering perhaps if some of the clever people who read this blog can come up with a clean but fast implementation.

To start off, here's a mostly unoptimized simple implementation of the standard qsort():


static void swap_internal(char *a, char *b, size_t size)
{
if (a != b)
{
char t;
while (size--)
{
t = *a;
*a++ = *b;
*b++ = t;
}
}
}

static void qsort_internal(char *begin, char *end, size_t size, int(*compar)(const void *, const void *))
{
if (end > begin)
{
char *pivot = begin;
char *l = begin + size, *r = end;

while (l < r)
{
if (compar(l, pivot) <= 0)
{
l += size;
}
else
{
r -= size;
swap_internal(l, r, size);
}
}
l -= size;
swap_internal(begin, l, size);
qsort_internal(begin, l, size, compar);
qsort_internal(r, end, size, compar);
}
}

void qsort(void *base, size_t nmemb, size_t size, int(*compar)(const void *, const void *))
{
qsort_internal((char *)base, (char *)base+nmemb*size, size, compar);
}


Note, this implementation is ~40 lines and can be improved. It can be improved with a better pivot selection, removing the recursion, and throwing in an insertion sort for certain cases.

I'm challenging my readers to give me a good clean implementation, which is under 80 lines (bad indentation doesn't count, I'll be running submitted code through my formatter), compliant, and isn't a total wreck. Creating some helper functions to make it clearer is a good idea too. Almost every implementation I've seen makes use of gotos to all over the place, and has returns smack in the middle of the function.

It'd be nice to bring something so useful into the new millennium, instead of using messy or slow implementations from the 70s.

If I get some nice implementations in, I'll do another piece on how to optimize Quicksort for sorting certain types/structures.