Efficient HV fetches without recomputing the key hash

[email protected] ("Paul \"LeoNerd\" Evans") Wed, 13 May 2026 18:42:52 +0100
Newsgroups perl.perl5.porters
Message-ID <[email protected]>
The HV API is a bit of a mess of functions. There's basically two ways
you can look up an item out of an HV, depending on how you have your
key:

   SV **  hv_fetch (HV *hv, const char *key, I32 klen, I32 lval);

   HE *  hv_fetch_ent(HV *hv, SV *keysv, I32 lval, U32 hash);

If you have your key as a plain char*+length pair, you can call
`hv_fetch`. If you have your key as a full SV already, you can call
`hv_fetch_ent`. The latter function allows you to pass in a precomputed
hash value; the former does not. That means if you call `hv_fetch` it
has to recalculate the hash value. That's not very efficient, if that
is already known.

When might it be known? Well,... maybe you already have it from an HE*
out of a different hash. Consider the following code chunk, which is
doing the C version of:

  $dst_hv{$_} += $src_hv{$_} for keys %src_hv;

We're going to call sv_inc() on each (possibly-new) element in dst_hv,
which we look up using each of the keys and values taken from src_hv:

  hv_iterinit(src_hv);

  HE *he;
  while((he = hv_iternext(src_hv)) {
    SV *src_sv = HeVAL(he);

    /* it's a shame I can't pass in HeHASH(he) here */
    SV **dst_svp = hv_fetch(dst_hv, HeKEY(he), HeKLEN(he), 1);

    sv_inc(*dst_svp, SvIV(src_sv));
  }

Here `hv_fetch()` needs to recalculate the hash values every time. If I
want to avoid that, I have to call `hv_fetch_ent` instead, and present
it with the key in an SV somehow. Calling `HeSVKEY_force()` would give
me that, but that's going to be just as inefficient as it has to copy
the string buffer.

I *believe* on a quick read through hv.c, that `hv_fetch` isn't going
to store or modify the keysv that is passed into it, so I might be safe
to do subtle "just reuse the string buffer pointer" trickery with it;
via something like:

  SV *keysv = newSV(0);
  SvPOK_on(keysv);
  SAVEFREESV(keysv);

  HE *he;
  while((he = hv_iternext(src_hv)) {
    SV *src_sv = HeVAL(he);

    SvPVX(keysv) = HeKEY(he);
    SvCUR_set(keysv, HeKLEN(he));
    /* should also do something about the UTF-8 flag here but I'm
     * omitting that for clarity of this example ;) */

    HE *dst_he = hv_fetch_ent(dst_hv, keysv, 0, HeHASH(he));
    ...
  }

That looks pretty hacky and subtle though. Is this the best approach?

Alternative ideas:

  * Directly reach-around the `hv_fetch*` macros by observing that
    they are all just wrappers around hv_common(), and make myself a
    brand new wrapper, something like

      #define hv_fetch_with_hash(hv, key, klen, lval, hash)    \
        hv_common_key_len((hv), (key), (klen),                 \
          (HV_FETCH_JUST_SV | ((lval) ? HV_FETCH_LVALUE : 0)), \
          NULL, (hash))

    That's a straight-up copy of hv_fetch, just passing in (hash)
    rather than 0 for the hash value.

    Downside: not guaranteed to continue to work in newer perls

  * Add a new API function with this shape, and then call it.

      SV **hv_fetch_with_hash(HV *hv, const char *key, I32 klen,
          I32 lval, U32 hash);

    Downside: doesn't work on older perls.

Maybe we could do both - define a new wrapper, and also suggest that
folks can call the hv_common directly with it on older perls?


Also, all of the above comments can be made also about "exists" and
"delete" operations. But not "store", because even the char*+length
taking `hv_store` function *does* take a `U32 hash` parameter.

---

Overall, I think it's a bit of a mess that these functions all eat an
entire `I32 lval` parameter simply for its truth. That really ought to
have been a `U32 flags`, which could then be used to contain a bunch of
different behaviours... but eh. It's a bit late now to start fixing all
of these things...

-- 
Paul "LeoNerd" Evans

[email protected]
http://www.leonerd.org.uk/  |  https://metacpan.org/author/PEVANS