Live data from Hacker News

Show HN: A hash array-mapped trie implementation in C

github.com

11–20 of 59 posts

Re: Show HN: A hash array-mapped trie implementation in C

#11
post #10

how does HAMTs compare with more recent designs like Swiss Tables? [1] [1] https://abseil.io/about/design/swisstables

Swiss Tables are hash tables. O(1) lookup, but expensive to copy.

HAMTs are hash tries. O(log(n)) lookup, but persistent / cheep to copy.

They are not really comparable, since hash tables are not persistent.

In functional languages, persistent data structures are MUCH more natural to work with. HAMTs we're originally created for the Clojure standard library, IIRC.

HAMTs lend themselves to more elegant/performant implementations than self-balancing trees, since they don't need to rebalance as long as your hash function is good.

Also, HAMTs generally have a high branching factor, so the search can be as fast as a hash table for small-to-medium maps. Though I don't know of any HAMTs using SIMD tricks like Swiss Table.

(EDIT: I guess the popcnt thing that HAMTs do would be considered a SIMD trick. Larger registers would allow the branching factor to be raised.)

Like hash tables, HAMTs don't require intermediate key comparison for lookup.

The original HAMT paper is a good read: http://infoscience.epfl.ch/record/64398/files/idealhashtrees...

Re: Show HN: A hash array-mapped trie implementation in C

#12
post #5

Earlier quoted context omitted.

If you want to "shim" the API, then it's easier to have the whole previous object, rather than just the cookie.

You don't need the cookie then. You can just allocate a larger struct (sort of subclassing it). You save a pointer indirection.

There's a limit to what can be crammed into a HN comment!

Re: Show HN: A hash array-mapped trie implementation in C

#14
How does this compare to https://github.com/arximboldi/immer (other than the C/C++ difference)?

Also, it's my understanding that, in practice, persistent data structures require a garbage collector in order to handle deallocation when used in a general-purpose way. How does your implementation handle that?

Also, have you seen https://github.com/cnuernber/ham-fisted ? I think there are a few other Java-based persistent collections as well in the overall Clojure ecosystem that also improve on Hickey's original implementation, but I can't recall them now…

Re: Show HN: A hash array-mapped trie implementation in C

#15
post #12

Earlier quoted context omitted.

You don't need the cookie then. You can just allocate a larger struct (sort of subclassing it). You save a pointer indirection.

There's a limit to what can be crammed into a HN comment!

I have discovered a truly marvelous allocator pattern which this HN comment is too small to contain.

Re: Show HN: A hash array-mapped trie implementation in C

#17
post #3

Just a note about your 'exported memory allocation' API: struct hamt_allocator { void *(*malloc)(const size_t size); void *(*realloc)(void *chunk, const size_t size); void (*free)(void *chunk); }; This whole thing could just be: struct hamt_allocator { void *cookie; void* (*realloc) (struct hamt_allocator* h, void* chk, const size_t size); }; With the following constraints: 1. `realloc(H, nullptr, N)` -- allocated N…

That's also what Lua does: https://www.lua.org/manual/5.3/manual.html#lua_Alloc

Re: Show HN: A hash array-mapped trie implementation in C

#18
post #3

Just a note about your 'exported memory allocation' API: struct hamt_allocator { void *(*malloc)(const size_t size); void *(*realloc)(void *chunk, const size_t size); void (*free)(void *chunk); }; This whole thing could just be: struct hamt_allocator { void *cookie; void* (*realloc) (struct hamt_allocator* h, void* chk, const size_t size); }; With the following constraints: 1. `realloc(H, nullptr, N)` -- allocated N…

I would drop the API directly and concentrate on the algorithm.

If users integrating HAMT need a different allocator situation, they can solve that problem by themselves, without a run-time indirection shim.

You can help those users by providing some macros somewhere like #define hamt_malloc(ctx, x) malloc(x) and so forth, so it can be retargeted in one place. Leave an ignored context argument in place for those who would like to have a run-time switch per-instance.

Re: Show HN: A hash array-mapped trie implementation in C

#19
post #12

Earlier quoted context omitted.

You don't need the cookie then. You can just allocate a larger struct (sort of subclassing it). You save a pointer indirection.

There's a limit to what can be crammed into a HN comment!

The solution is obviously to realloc the comment.

Re: Show HN: A hash array-mapped trie implementation in C

#20
post #3

Just a note about your 'exported memory allocation' API: struct hamt_allocator { void *(*malloc)(const size_t size); void *(*realloc)(void *chunk, const size_t size); void (*free)(void *chunk); }; This whole thing could just be: struct hamt_allocator { void *cookie; void* (*realloc) (struct hamt_allocator* h, void* chk, const size_t size); }; With the following constraints: 1. `realloc(H, nullptr, N)` -- allocated N…

Sadly, this pattern doesn't work with standard realloc() anymore. C23 makes this undefined behavior due to existing non-conforming implementations. https://www.open-std.org/jtc1/sc22/wg14/www/docs/n2464.pdf

The pattern was never specified as fully working. Not in C99 and C90.

It's due to the following reason: it was never specified that realloc(ptr, 0) behaves like free(ptr).

The case of size == 0 is not separately discussed in the C99 description of realloc.

realloc(ptr, 0) can behave like (free(ptr), malloc(0)), where malloc(0) doesn't necessarily behave like ((void *) 0). Malloc may return a unique object that may be later freed.

That is to say, realloc can effectively reduce an object to zero size without freeing it, or possibly even free an object only to replace it with a different, unique zero-sized object.

Note that in the case when ptr == NULL, which is discussed in C99, realloc behaves like malloc. So in the case realloc(NULL, 0), we know that it's the same as malloc(0). That's a case when size == 0, and realloc is required to behave like malloc.

This is a standard-conforming realloc, and always has been:

  void *realloc(void *oldptr, size_t newsize)
  {
    size_t oldsize = __allocated_size(oldptr); // this handles NULL, and returns 0

    void *newptr = malloc(newsize);

    if (newptr != NULL && oldptr != NULL)
       memcpy(newptr, oldptr, min(newsize, oldsize);
    
    return newptr;
  }
This realloc will not behave like free if malloc(0) returns unique pointers.

If you want a realloc-like function that behaves like an "all in one allocator", you have to write your own:

   void *git_reset_of_allocators(void *oldptr, size_t newsize)
   {
      if (size == 0) {
        free(oldptr);
        return NULL;
      } else { 
        // handles all other cases OK
        return realloc(oldptr, newsize);
      }
   }
Post reply on HN