Live data from Hacker News

Finger Trees: A Simple General-Purpose Data Structure (2006)

staff.city.ac.uk

71–79 of 79 posts

Re: Finger Trees: A Simple General-Purpose Data Structure (2006)

#71

This is a fun data structure to implement in Haskell, and I've always been curious about how one would do it in C++, largely due to the fact that data FingerTree a = Empty | Single a | Deep (Digit a) (FingerTree (Node a)) (Digit a) Has polymorphic recursion in the last case. What would be the C++ approach for dealing with this sort of thing? Pass in a compile time integer to represent the level of nesting?

I've written this several times in several ways using C++ (and I'll probably write it at least once more to make it better). In an early version I used an integer level as you came to, but it made the compiler very unhappy as it recursively tried to expand nested types at compile time (it couldn't figure out the recursive types would terminate in practice). Max template recursion of 256 if I remember correctly, even though you'd never instantiate past 45 or so on any machine in the world.

In a later version, I implemented the specializations as inheritance on the abstract FingerTree base class (verbose, but it works), and I added Leafs and Nodes. Leafs are FingerTrees that hold your data, and Nodes are FingerTrees that point to other FingerTree instance. This dodges the recursive types problem. I don't know much Haskell, but I think it would be the C++ equivalent of:

    data FingerTree a = EmptyLeaf
                    | SingleLeaf a
                    | DoubleLeaf a a
                    | TripleLeaf a a a
                    | EmptyNode
                    | SingleNode (FingerTree a)
                    | DoulbeNode (FingerTree a) (FingerTree a)
                    | TripleNode (FingerTree a) (FingerTree a) (FingerTree a)
                    | FingerSpine (FingerTree a) (FingerTree a) (FingerTree a)
Virtual methods on each specialization took the place of pattern matching. Not super elegant.

Re: Finger Trees: A Simple General-Purpose Data Structure (2006)

#72
post #60

Earlier quoted context omitted.

File systems tend not to have large data structures. They are relatively modest data structures which manage bulk access to large blocks of data. A subtle distinction, but important in this context.

Really? Even huge volumes with loads of tiny files? Mail and Usenet servers? Build servers? What is your definition of large and modest here?

The data structures under discussion here are those used to track individual files. Yes, there can be squillions of them, but as data structures, they are, in fact, rather modest.

Re: Finger Trees: A Simple General-Purpose Data Structure (2006)

#73

A few years ago, our research group published a data structure, called Chunked Sequence, that is similar to the Finger Tree. In short, Chunked Sequence features the same asymptotic profile as does Finger Tree (neglecting persistence) and, in addition, offers strong guarantees with respect to constant factors. Very roughly speaking, Chunked Sequence is to Finger Tree what b-tree is to red-black tree. We've implemented…

Cool. But please provide clear licensing information (preferably a permissive free software license). Right now I can't find anything at all about what terms you are releasing this source code under. Therefore most people will be unable to use it, which is a shame.

Thanks for the comment! We are using the MIT license. I've updated the source code in the git repository accordingly.

Re: Finger Trees: A Simple General-Purpose Data Structure (2006)

#74
post #70
post #45

Earlier quoted context omitted.

Where I can find info about pagetable trie?

I'm interested too. I've been thinking about the idea, and it seems like you'd have to be sure to re-use the hell out of your file descriptor on /dev/zero (otherwise, you could easily run out of file descriptors). It also seems like you're trading cache misses for system calls if you have to re-map pages a lot. Maybe it's a clear win, but I'd like to understand it better.

No need for /dev/zero: Linux has memfd[1] and OSX has vm_remap[2]. You only need one file descriptor per heap because Linux lets you poke holes with fallocate[3].

I'll define objects with a header that looks roughly like this:

    struct header {
      off_t phys;
      size_t len;
      int localrefs;
      short flags;
      char mstrategy, type;
    };
phys is the offset in the heap file descriptor. len is the number of units and type is indexed into an array of unit sizes.

mstrategy is used to select the bucket size (allocated range is a power of two, so 1localrefs is an optimisation which I'll get to.

If I want to allocate an array of type t, size n, I can use a BSR[4] to identify the bucket that it needs, and see if there's anything on the free list. If there isn't, I can see if I can split a larger bucket into two parts (this is effectively Knuth's buddy allocator).

I know that (mstrategy&31) = BSR(page size) I can take a virtual address of a bigger region, then either mmap the memfd (using phys) or vm_remap the region into the bigger region. Instead of copying the contents of the pages, the operating system will simply copy the page table (which is 3 levels deep[5], hence the log log log, although using 1G pages means log log with a lower coefficient). This is a tremendous win for problems that need to deal with several large arrays.

Now localrefs gives me a further optimisation: In-process, I can track the reference count of objects, and if my functions always consume their arguments I know inside the grow/append/prepend routine if this is the only holder of the object. If it is, I can potentially reuse this virtual address immediately, saving 3-4 syscalls.

When it's time to deallocate, I can put small objects on my free list, and garbage collect any big objects by calling fallocate() on the physical address to poke a hole (freeing system memory). OSX doesn't need fallocate() because mach has vm_unmap.

[1]: https://dvdhrm.wordpress.com/2014/06/10/memfd_create2/

[2]: http://web.mit.edu/darwin/src/modules/xnu/osfmk/man/vm_remap... because the osx manual page is pants

[3]: http://man7.org/linux/man-pages/man2/fallocate.2.html

[4]: http://x86.renejeschke.de/html/file_module_x86_id_20.html

[5]: http://wiki.osdev.org/Page_Tables#Long_mode_.2864-bit.29_pag...

> It also seems like you're trading cache misses for system calls if you have to re-map pages a lot

Cache misses aren't the dominant force here.

The real trade off is illustrated with benchmarking: memcpy one page, versus 8 bytes (page table entry). How many pages do you need to copy before it is faster to pay the fixed (Memory streams at a rate of around 10GB/sec, but the TLB flush is ~100ns and the memory latency is only around 10ns, so it's easy to see how quick the gains add up when you're using 1GB pages.

Re: Finger Trees: A Simple General-Purpose Data Structure (2006)

#75
post #45
post #12

Earlier quoted context omitted.

So do real arrays on real hardware (pagetable trie). Small arrays are contiguous so memcpy is fast. For large arrays, if you create a dummy file (or uses a memfd) you can mmap a region, and then copy it by mmaping the same region with the appropriate flags. You can resize (grow) it almost as easily. They're so much faster than linked lists, that there's no (performance) reason to use linked-lists on real hardware.

Where I can find info about pagetable trie?

The page table on x86/amd64 is a trie. OSDEV[1] has a pretty good page on it.

All you need to remember is that this is what is inside the operating system and hardware layers, so you're already paying for it. What I propose is taking advantage of it. If you can arrange things correctly (see my other comment describing this more fully[2]) you might find things that seem expensive (when dealing only with von neumann memory) are suddenly very cheap when you accept you're programming a piece of real hardware.

[1]: http://wiki.osdev.org/Page_Tables

[2]: https://news.ycombinator.com/item?id=13269288

Re: Finger Trees: A Simple General-Purpose Data Structure (2006)

#76

Earlier quoted context omitted.

Cool. But please provide clear licensing information (preferably a permissive free software license). Right now I can't find anything at all about what terms you are releasing this source code under. Therefore most people will be unable to use it, which is a shame.

Thanks for the comment! We are using the MIT license. I've updated the source code in the git repository accordingly.

Which MIT license?

Re: Finger Trees: A Simple General-Purpose Data Structure (2006)

#77

Earlier quoted context omitted.

Cool. But please provide clear licensing information (preferably a permissive free software license). Right now I can't find anything at all about what terms you are releasing this source code under. Therefore most people will be unable to use it, which is a shame.

Thanks for the comment! We are using the MIT license. I've updated the source code in the git repository accordingly.

http://choosealicense.com/

Re: Finger Trees: A Simple General-Purpose Data Structure (2006)

#78

Earlier quoted context omitted.

Thanks for the comment! We are using the MIT license. I've updated the source code in the git repository accordingly.

Which MIT license?

Thanks again! We're using the Expat license.

Re: Finger Trees: A Simple General-Purpose Data Structure (2006)

#79
post #49

The problem with using lazy data structures for storing things is that deletes don't necessarily free up storage. Fingertrees are fine when you need to store a collection in order to implement some algorithm (and being persistent makes it useful if this algorithm has to backtrack), but they aren't so good for backing a collection that changes over time, like the set of currently connected sockets in a network service…

Looking at Haskell's Data.Sequence, it looks like it's strict in its elements. So a delete does free up memory as long as the deleted object isn't being used somewhere else.

It needs to be spine strict for that to work, but the amortization for finger trees doesn't work without laziness
Post reply on HN