Am 14.08.2018 um 03:58 schrieb Jeff King:
> Your suggestion can be implemented using khash (my patch below).
>
>> Before:
>> Benchmark #1: ./git-cat-file --batch-all-objects --buffer --unordered
>> --batch-check='%(objectname)'
>>
>> Time (mean ± σ): 269.5 ms ± 26.7 ms [User: 247.7 ms, System: 21.4
>> ms]
>>
>> Range (min … max): 240.3 ms … 339.3 ms
>>
>> After:
>> Benchmark #1: ./git-cat-file --batch-all-objects --buffer --unordered
>> --batch-check='%(objectname)'
>>
>> Time (mean ± σ): 224.2 ms ± 18.2 ms [User: 201.7 ms, System: 22.1
>> ms]
>>
>> Range (min … max): 205.0 ms … 259.0 ms
>
> Yeah. My best-of-five dropped from 300ms to 247ms. That 300 was using
> the memory pool, though khash's deletion strategy isn't all that
> different (the wasted memory hangs around until the next hash resize,
> but if you're evenly dropping and adding, you likely won't need to
> resize).
With your khash patch:
Benchmark #1: ./git-cat-file --batch-all-objects --buffer --unordered
--batch-check='%(objectname)'
Time (mean ± σ): 159.1 ms ± 20.5 ms [User: 140.3 ms, System: 18.5 ms]
Range (min … max): 140.0 ms … 214.0 ms
So it seems worth it.
> Anyway, here's the khash patch for reference.
>
> diff --git a/fetch-pack.c b/fetch-pack.c
> index 5714bcbddd..5a86b10a5e 100644
> --- a/fetch-pack.c
> +++ b/fetch-pack.c
> @@ -534,7 +534,7 @@ static int tip_oids_contain(struct oidset *tip_oids,
> * add to "newlist" between calls, the additions will always be for
> * oids that are already in the set.
> */
> - if (!tip_oids->map.map.tablesize) {
> + if (!tip_oids->map) {
> add_refs_to_oidset(tip_oids, unmatched);
> add_refs_to_oidset(tip_oids, newlist);
> }
The caller shouldn't look at the private parts of the implementation
like this. tablesize is the allocation count, which becomes non-zero
if at least one entry was added. tip_oids is only inserted into, never
deleted from, so a count or size function could be used instead as a
cleaner interface. (In a separate patch..)
> diff --git a/oidset.c b/oidset.c
> index 454c54f933..2964b43b2d 100644
> --- a/oidset.c
> +++ b/oidset.c
> @@ -3,38 +3,44 @@
>
> int oidset_contains(const struct oidset *set, const struct object_id *oid)
> {
> - if (!set->map.map.tablesize)
> + khiter_t pos;
> +
> + if (!set->map)
> return 0;
> - return !!oidmap_get(&set->map, oid);
> +
> + pos = kh_get_oid(set->map, *oid);
> + return pos < kh_end(set->map);
> }
>
> int oidset_insert(struct oidset *set, const struct object_id *oid)
> {
> - struct oidmap_entry *entry;
> + int hash_ret;
>
> - if (!set->map.map.tablesize)
> - oidmap_init(&set->map, 0);
> - else if (oidset_contains(set, oid))
> - return 1;
> + if (!set->map)
> + set->map = kh_init_oid();
>
> - entry = xmalloc(sizeof(*entry));
> - oidcpy(&entry->oid, oid);
> -
> - oidmap_put(&set->map, entry);
> - return 0;
> + kh_put_oid(set->map, *oid, &hash_ret);
> + return !hash_ret;
> }
So initialization is deferred to the first insert, and the empty set
can be represented in two ways -- map == NULL and map->size == 0.
I wondered about the performance impact of all those NULL checks at
insert and lookup and converted it to stack storage, with a dirty
hand-rolled oidset_clear() implementation. It wasn't any faster.
>
> int oidset_remove(struct oidset *set, const struct object_id *oid)
> {
> - struct oidmap_entry *entry;
> + khiter_t pos;
>
> - entry = oidmap_remove(&set->map, oid);
> - free(entry);
> + if (!set->map)
> + return 0;
> +
> + pos = kh_get_oid(set->map, *oid);
> + if (pos < kh_end(set->map)) {
> + kh_del_oid(set->map, pos);
> + return 1;
> + }
>
> - return (entry != NULL);
> + return 0;
> }
>
> void oidset_clear(struct oidset *set)
> {
> - oidmap_free(&set->map, 1);
> + kh_destroy_oid(set->map);
> + set->map = NULL;
> }
> diff --git a/oidset.h b/oidset.h
> index 40ec5f87fe..4c4c5a42fe 100644
> --- a/oidset.h
> +++ b/oidset.h
> @@ -2,6 +2,7 @@
> #define OIDSET_H
>
> #include "oidmap.h"
This can go.
> +#include "khash.h"
>
> /**
> * This API is similar to sha1-array, in that it maintains a set of object
> ids
> @@ -15,19 +16,34 @@
> * table overhead.
> */
>
> +static inline unsigned int oid_hash(const struct object_id oid)
> +{
> + unsigned int hash;
> + memcpy(&hash, oid.hash, sizeof(hash));
> + return hash;
> +}
> +
> +static inline int oid_equal(const struct object_id a,
> + const struct object_id b)
> +{
> + return !oidcmp(&a, &b);
> +}
Look, it's oideq() from that other series in disguise! :)
> +
> +KHASH_INIT(oid, struct object_id, int, 0, oid_hash, oid_equal)
Note to self: The 0 is for kh_is_map, which means that no values are
kept and the given value type (int) doesn't matter.
> +
> +
> /**
> * A single oidset; should be zero-initialized (or use OIDSET_INIT).
> */
> struct oidset {
> - struct oidmap map;
> + kh_oid_t *map;
> };
>
> -#define OIDSET_INIT { OIDMAP_INIT }
> -
> +#define OIDSET_INIT { NULL }
>
> static inline void oidset_init(struct oidset *set, size_t initial_size)
> {
> - oidmap_init(&set->map, initial_size);
> + set->map = NULL;
> }
This ignores initial_size, which is misleading. We should probably
call kh_resize_oid() here and move the function to oidset.c. Or
get rid of the second parameter..
>
> /**
> @@ -58,19 +74,25 @@ int oidset_remove(struct oidset *set, const struct
> object_id *oid);
> void oidset_clear(struct oidset *set);
>
> struct oidset_iter {
> - struct oidmap_iter m_iter;
> + kh_oid_t *map;
> + khiter_t iter;
> };
>
> static inline void oidset_iter_init(struct oidset *set,
> struct oidset_iter *iter)
> {
> - oidmap_iter_init(&set->map, &iter->m_iter);
> + iter->map = set->map;
> + iter->iter = kh_begin(iter->map);
> }
This is fine even if map == NULL, because kh_begin() can handle any
parameter value, as it ignores them...
>
> static inline struct object_id *oidset_iter_next(struct oidset_iter *iter)
> {
> - struct oidmap_entry *e = oidmap_iter_next(&iter->m_iter);
> - return e ? &e->oid : NULL;
> + for (; iter->iter != kh_end(iter->map); iter->iter++) {
> + if (!kh_exist(iter->map, iter->iter))
> + continue;
> + return &kh_key(iter->map, iter->iter);
> + }
> + return NULL;
> }
... but kh_end() dereferences map, so iterating a fresh oidset will
segfault here.
>
> static inline struct object_id *oidset_iter_first(struct oidset *set,
>