diff --git a/arbor/benchmark_cell_group.cpp b/arbor/benchmark_cell_group.cpp
index bdcb1789a6e62b28ea3b72ca548dc2cc1756f8b4..36887fc8ea4e00e361427384250ff6244ceed2aa 100644
--- a/arbor/benchmark_cell_group.cpp
+++ b/arbor/benchmark_cell_group.cpp
@@ -1,5 +1,4 @@
 #include <chrono>
-#include <exception>
 
 #include <arbor/arbexcept.hpp>
 #include <arbor/benchmark_cell.hpp>
@@ -40,8 +39,8 @@ benchmark_cell_group::benchmark_cell_group(const std::vector<cell_gid_type>& gid
     for (const auto& c: cells_) {
         cg_sources.add_cell();
         cg_targets.add_cell();
-        cg_sources.add_label(c.source, {0, 1});
-        cg_targets.add_label(c.target, {0, 1});
+        cg_sources.add_label(hash_value(c.source), {0, 1});
+        cg_targets.add_label(hash_value(c.target), {0, 1});
     }
 
     benchmark_cell_group::reset();
diff --git a/arbor/cable_cell.cpp b/arbor/cable_cell.cpp
index 3afbc82400effd0bbbecd9b4f91df7bae7a7c871..210fdd3cea655f3ee3e70f12d2ce1578660d6e62 100644
--- a/arbor/cable_cell.cpp
+++ b/arbor/cable_cell.cpp
@@ -88,7 +88,7 @@ struct cable_cell_impl {
     decor decorations;
 
     // The placeable label to lid_range map
-    dynamic_typed_map<constant_type<std::unordered_multimap<cell_tag_type, lid_range>>::type> labeled_lid_ranges;
+    dynamic_typed_map<constant_type<std::unordered_multimap<hash_type, lid_range>>::type> labeled_lid_ranges;
 
     cable_cell_impl(const arb::morphology& m, const label_dict& labels, const decor& decorations):
         provider(m, labels),
@@ -120,7 +120,7 @@ struct cable_cell_impl {
     }
 
     template <typename Item>
-    void place(const locset& ls, const Item& item, const cell_tag_type& label) {
+    void place(const locset& ls, const Item& item, const hash_type& label) {
         auto& mm = get_location_map(item);
         cell_lid_type& lid = placed_count.get<Item>();
         cell_lid_type first = lid;
@@ -226,7 +226,8 @@ void cable_cell_impl::init(const decor& d) {
     for (const auto& p: d.placements()) {
         auto& where = std::get<0>(p);
         auto& label = std::get<2>(p);
-        std::visit([this, &where, &label] (auto&& what) {return this->place(where, what, label);}, std::get<1>(p));
+        std::visit([this, &where, &label] (auto&& what) {return this->place(where, what, label); },
+                   std::get<1>(p));
     }
 }
 
@@ -280,16 +281,21 @@ const cable_cell_parameter_set& cable_cell::default_parameters() const {
     return impl_->decorations.defaults();
 }
 
-const std::unordered_multimap<cell_tag_type, lid_range>& cable_cell::detector_ranges() const {
+const cable_cell::lid_range_map& cable_cell::detector_ranges() const {
     return impl_->labeled_lid_ranges.get<threshold_detector>();
 }
 
-const std::unordered_multimap<cell_tag_type, lid_range>& cable_cell::synapse_ranges() const {
+const cable_cell::lid_range_map& cable_cell::synapse_ranges() const {
     return impl_->labeled_lid_ranges.get<synapse>();
 }
 
-const std::unordered_multimap<cell_tag_type, lid_range>& cable_cell::junction_ranges() const {
+const cable_cell::lid_range_map& cable_cell::junction_ranges() const {
     return impl_->labeled_lid_ranges.get<junction>();
 }
 
+cell_tag_type decor::tag_of(hash_type hash) const {
+    if (!hashes_.count(hash)) throw arbor_internal_error{util::pprintf("Unknown hash for {}.", std::to_string(hash))};
+    return hashes_.at(hash);
+}
+
 } // namespace arb
diff --git a/arbor/cable_cell_param.cpp b/arbor/cable_cell_param.cpp
index 909ec7fabaae7b837b0d549e6d7d981a7e752ecd..e3b425de7a45c956117d31184fedbe583975cada 100644
--- a/arbor/cable_cell_param.cpp
+++ b/arbor/cable_cell_param.cpp
@@ -1,7 +1,4 @@
-#include <cfloat>
 #include <cmath>
-#include <memory>
-#include <numeric>
 #include <vector>
 #include <variant>
 #include <tuple>
@@ -10,7 +7,9 @@
 #include <arbor/cable_cell_param.hpp>
 #include <arbor/s_expr.hpp>
 
+#include <arbor/util/hash_def.hpp>
 #include "util/maputil.hpp"
+#include "util/strprintf.hpp"
 
 namespace arb {
 
@@ -120,7 +119,12 @@ decor& decor::paint(region where, paintable what) {
 }
 
 decor& decor::place(locset where, placeable what, cell_tag_type label) {
-    placements_.emplace_back(std::move(where), std::move(what), std::move(label));
+    auto hash = hash_value(label);
+    if (hashes_.count(hash) && hashes_.at(hash) != label) {
+        throw arbor_internal_error{util::strprintf("Hash collision {} ./. {}", label, hashes_.at(hash))};
+    }
+    placements_.emplace_back(std::move(where), std::move(what), hash);
+    hashes_.emplace(hash, label);
     return *this;
 }
 
diff --git a/arbor/communication/mpi_context.cpp b/arbor/communication/mpi_context.cpp
index 6019e76065ecfe1360e6b95d2cab94d8f1646b93..c2fcf64af76ffa0f226ad23e7e4ef27a87916b0a 100644
--- a/arbor/communication/mpi_context.cpp
+++ b/arbor/communication/mpi_context.cpp
@@ -59,13 +59,11 @@ struct mpi_context_impl {
     }
 
     cell_label_range gather_cell_label_range(const cell_label_range& local_ranges) const {
-        std::vector<cell_size_type> sizes;
-        std::vector<cell_tag_type> labels;
-        std::vector<lid_range> ranges;
-        sizes  = mpi::gather_all(local_ranges.sizes(), comm_);
-        labels = mpi::gather_all(local_ranges.labels(), comm_);
-        ranges = mpi::gather_all(local_ranges.ranges(), comm_);
-        return cell_label_range(sizes, labels, ranges);
+        cell_label_range res;
+        res.sizes  = mpi::gather_all(local_ranges.sizes, comm_);
+        res.labels = mpi::gather_all(local_ranges.labels, comm_);
+        res.ranges = mpi::gather_all(local_ranges.ranges, comm_);
+        return res;
     }
 
     cell_labels_and_gids gather_cell_labels_and_gids(const cell_labels_and_gids& local_labels_and_gids) const {
diff --git a/arbor/fvm_lowered_cell_impl.hpp b/arbor/fvm_lowered_cell_impl.hpp
index 894fcab0d4725100092143ced5b390f039206c0e..94716b21c58c83dd774efdbc270191e7b82d322d 100644
--- a/arbor/fvm_lowered_cell_impl.hpp
+++ b/arbor/fvm_lowered_cell_impl.hpp
@@ -315,15 +315,27 @@ fvm_detector_info get_detector_info(arb_size_type max,
     return { max, std::move(cv), std::move(threshold), ctx };
 }
 
-template <typename Backend>
-void fvm_lowered_cell_impl<Backend>::add_probes(const std::vector<cell_gid_type>& gids,
-                                                const std::vector<cable_cell>& cells,
-                                                const recipe& rec,
-                                                const fvm_cv_discretization& D,
-                                                const std::unordered_map<std::string, mechanism*>& mechptr_by_name,
-                                                const fvm_mechanism_data& mech_data,
-                                                const std::vector<target_handle>& target_handles,
-                                                probe_association_map& probe_map) {
+inline cell_size_type
+add_labels(cell_label_range& clr, const cable_cell::lid_range_map& ranges) {
+    clr.add_cell();
+    cell_size_type count = 0;
+    std::unordered_map<hash_type, cell_tag_type> hashes;
+    for (const auto& [label, range]: ranges) {
+        clr.add_label(label, range);
+        count += (range.end - range.begin);
+    }
+    return count;
+}
+
+template <typename Backend> void
+fvm_lowered_cell_impl<Backend>::add_probes(const std::vector<cell_gid_type>& gids,
+                                           const std::vector<cable_cell>& cells,
+                                           const recipe& rec,
+                                           const fvm_cv_discretization& D,
+                                           const std::unordered_map<std::string, mechanism*>& mechptr_by_name,
+                                           const fvm_mechanism_data& mech_data,
+                                           const std::vector<target_handle>& target_handles,
+                                           probe_association_map& probe_map) {
     auto ncell = gids.size();
 
     std::vector<fvm_probe_data> probe_data;
@@ -343,9 +355,9 @@ void fvm_lowered_cell_impl<Backend>::add_probes(const std::vector<cell_gid_type>
     }
 }
 
-template <typename Backend>
-fvm_initialization_data fvm_lowered_cell_impl<Backend>::initialize(const std::vector<cell_gid_type>& gids,
-                                                                   const recipe& rec) {
+template <typename Backend> fvm_initialization_data
+fvm_lowered_cell_impl<Backend>::initialize(const std::vector<cell_gid_type>& gids,
+                                           const recipe& rec) {
     using std::any_cast;
     using util::count_along;
     using util::make_span;
@@ -375,28 +387,9 @@ fvm_initialization_data fvm_lowered_cell_impl<Backend>::initialize(const std::ve
     for (auto i : util::make_span(ncell)) {
         auto gid = gids[i];
         const auto& c = cells[i];
-
-        fvm_info.source_data.add_cell();
-        fvm_info.target_data.add_cell();
-        fvm_info.gap_junction_data.add_cell();
-
-        unsigned count = 0;
-        for (const auto& [label, range]: c.detector_ranges()) {
-            fvm_info.source_data.add_label(label, range);
-            count+=(range.end - range.begin);
-        }
-        fvm_info.num_sources[gid] = count;
-
-        count = 0;
-        for (const auto& [label, range]: c.synapse_ranges()) {
-            fvm_info.target_data.add_label(label, range);
-            count+=(range.end - range.begin);
-        }
-        fvm_info.num_targets[gid] = count;
-
-        for (const auto& [label, range]: c.junction_ranges()) {
-            fvm_info.gap_junction_data.add_label(label, range);
-        }
+        fvm_info.num_sources[gid] = add_labels(fvm_info.source_data, c.detector_ranges());
+        fvm_info.num_targets[gid] = add_labels(fvm_info.target_data, c.synapse_ranges());
+        add_labels(fvm_info.gap_junction_data, c.junction_ranges());
     }
 
     cable_cell_global_properties global_props;
diff --git a/arbor/include/arbor/cable_cell.hpp b/arbor/include/arbor/cable_cell.hpp
index 221c967d149d8bd1b15067d0f34acd16e920e90f..e119508a49a440ae6a3e3fb53c2e8df960b49f4f 100644
--- a/arbor/include/arbor/cable_cell.hpp
+++ b/arbor/include/arbor/cable_cell.hpp
@@ -246,8 +246,8 @@ using cable_cell_location_map = static_typed_map<location_assignment,
     synapse, junction, i_clamp, threshold_detector>;
 
 // High-level abstract representation of a cell.
-class ARB_SYMBOL_VISIBLE cable_cell {
-public:
+struct ARB_SYMBOL_VISIBLE cable_cell {
+    using lid_range_map = std::unordered_multimap<hash_type, lid_range>;
     using index_type = cell_lid_type;
     using size_type = cell_local_size_type;
     using value_type = double;
@@ -311,9 +311,9 @@ public:
     const cable_cell_parameter_set& default_parameters() const;
 
     // The labeled lid_ranges of sources, targets and gap_junctions on the cell;
-    const std::unordered_multimap<cell_tag_type, lid_range>& detector_ranges() const;
-    const std::unordered_multimap<cell_tag_type, lid_range>& synapse_ranges() const;
-    const std::unordered_multimap<cell_tag_type, lid_range>& junction_ranges() const;
+    const lid_range_map& detector_ranges() const;
+    const lid_range_map& synapse_ranges() const;
+    const lid_range_map& junction_ranges() const;
 
 private:
     std::unique_ptr<cable_cell_impl, void (*)(cable_cell_impl*)> impl_;
diff --git a/arbor/include/arbor/cable_cell_param.hpp b/arbor/include/arbor/cable_cell_param.hpp
index 46ed13e152c39beed666bd62d35360dfa85cdf76..59bb5bb0148843354c2678bee743be8cfd2e417a 100644
--- a/arbor/include/arbor/cable_cell_param.hpp
+++ b/arbor/include/arbor/cable_cell_param.hpp
@@ -1,12 +1,10 @@
 #pragma once
 
 #include <cmath>
-#include <memory>
 #include <optional>
 #include <unordered_map>
 #include <string>
 #include <variant>
-#include <any>
 
 #include <arbor/export.hpp>
 #include <arbor/arbexcept.hpp>
@@ -313,8 +311,9 @@ struct ARB_ARBOR_API cable_cell_parameter_set {
 // are to be applied to a morphology in a cable_cell.
 class ARB_ARBOR_API decor {
     std::vector<std::pair<region, paintable>> paintings_;
-    std::vector<std::tuple<locset, placeable, cell_tag_type>> placements_;
+    std::vector<std::tuple<locset, placeable, hash_type>> placements_;
     cable_cell_parameter_set defaults_;
+    std::unordered_map<hash_type, cell_tag_type> hashes_;
 
 public:
     const auto& paintings()  const {return paintings_;  }
@@ -324,6 +323,8 @@ public:
     decor& paint(region, paintable);
     decor& place(locset, placeable, cell_tag_type);
     decor& set_default(defaultable);
+
+    cell_tag_type tag_of(hash_type) const;
 };
 
 ARB_ARBOR_API extern cable_cell_parameter_set neuron_parameter_defaults;
diff --git a/arbor/include/arbor/common_types.hpp b/arbor/include/arbor/common_types.hpp
index ea97872fec204e4f596302ea57b143477a645006..be3f4f095d3231fbefd23c11066df4857a26087e 100644
--- a/arbor/include/arbor/common_types.hpp
+++ b/arbor/include/arbor/common_types.hpp
@@ -19,6 +19,10 @@
 
 namespace arb {
 
+// Internal hashes use this 64bit id
+
+using hash_type = std::size_t;
+
 // For identifying cells globally.
 
 using cell_gid_type = std::uint32_t;
diff --git a/arbor/include/arbor/cv_policy.hpp b/arbor/include/arbor/cv_policy.hpp
index a1a5077cf17dde69e9c21e5489bff15c1f17bc05..a56a5c51d97bac2899e7a26b5bae08534a927868 100644
--- a/arbor/include/arbor/cv_policy.hpp
+++ b/arbor/include/arbor/cv_policy.hpp
@@ -59,7 +59,7 @@
 
 namespace arb {
 
-class cable_cell;
+struct cable_cell;
 
 struct cv_policy_base {
     virtual locset cv_boundary_points(const cable_cell& cell) const = 0;
diff --git a/arbor/include/arbor/util/hash_def.hpp b/arbor/include/arbor/util/hash_def.hpp
index fc6770cc4125b30aa7f1282547c25f2a0e3a220d..933a3237e2654c9382eac349d8af1c56da75657b 100644
--- a/arbor/include/arbor/util/hash_def.hpp
+++ b/arbor/include/arbor/util/hash_def.hpp
@@ -17,27 +17,80 @@
  */
 
 #include <cstddef>
-#include <typeindex>
+#include <string_view>
+#include <functional>
 
 // Helpers for forming hash values of compounds objects.
-
 namespace arb {
 
-inline std::size_t hash_value_combine(std::size_t n) {
-    return n;
+namespace detail {
+
+// Non-cryptographic hash function for mapping strings to internal
+// identifiers. Concretely, FNV-1a hash function taken from
+//
+//   http://www.isthe.com/chongo/tech/comp/fnv/index.html
+//
+// NOTE: It may be worth it considering different hash functions in
+//       the future that have better characteristic, xxHash or Murmur
+//       look interesting but are more complex and likely require adding
+//       external dependencies.
+//       NOTE: this is the obligatory comment on a better hash function
+//             that will be here until the end of time.
+
+template <typename T>
+inline constexpr std::size_t internal_hash(T&& data) {
+    using D = std::decay_t<T>;
+    constexpr std::size_t prime = 0x100000001b3;
+    constexpr std::size_t offset_basis = 0xcbf29ce484222325;
+    static_assert(!std::is_pointer_v<D> || std::is_same_v<D, void*> || std::is_convertible_v<T, std::string_view>,
+                  "Pointer types except void* will not be hashed.");
+    if constexpr (std::is_convertible_v<T, std::string_view>) {
+        std::size_t hash = offset_basis;
+        for (uint8_t byte: std::string_view{data}) {
+            hash = hash ^ byte;
+            hash = hash * prime;
+        }
+        return hash;
+    }
+    if constexpr (std::is_integral_v<D>) {
+        unsigned long long bytes = data;
+        std::size_t hash = offset_basis;
+        for (int ix = 0; ix < sizeof(data); ++ix) {
+            uint8_t byte = bytes & 255;
+            bytes >>= 8;
+            hash = hash ^ byte;
+            hash = hash * prime;
+        }
+        return hash;
+    }
+    if constexpr (std::is_pointer_v<D>) {
+        unsigned long long bytes = reinterpret_cast<unsigned long long>(data);
+        std::size_t hash = offset_basis;
+        for (int ix = 0; ix < sizeof(data); ++ix) {
+            uint8_t byte = bytes & 255;
+            bytes >>= 8;
+            hash = hash ^ byte;
+            hash = hash * prime;
+        }
+        return hash;
+    }
+    return std::hash<D>{}(data);
 }
 
-template <typename... T>
-std::size_t hash_value_combine(std::size_t n, std::size_t m, T... tail) {
-    constexpr std::size_t prime2 = 54517;
-    return hash_value_combine(prime2*n + m, tail...);
+inline
+std::size_t hash_value_combine(std::size_t n) { return n; }
+
+template <typename T, typename... Ts>
+std::size_t hash_value_combine(std::size_t n, const T& head, const Ts&... tail) {
+    constexpr std::size_t prime = 54517;
+    return hash_value_combine(prime*n + internal_hash(head), tail...);
 }
 
-template <typename... T>
-std::size_t hash_value(const T&... t) {
-    constexpr std::size_t prime1 = 93481;
-    return hash_value_combine(prime1, std::hash<T>{}(t)...);
 }
+
+// User facing API
+template <typename... T>
+std::size_t hash_value(const T&... ts) { return detail::hash_value_combine(0, ts...); }
 }
 
 #define ARB_DEFINE_HASH(type,...)\
diff --git a/arbor/label_resolution.cpp b/arbor/label_resolution.cpp
index dd928098b653b74287a579c60c5ac27cfa0d56b4..29c8b7645423956c5c796ee02ccb092e95b6bad6 100644
--- a/arbor/label_resolution.cpp
+++ b/arbor/label_resolution.cpp
@@ -1,14 +1,15 @@
 #include <iterator>
 #include <vector>
+#include <numeric>
 
 #include <arbor/assert.hpp>
 #include <arbor/arbexcept.hpp>
 #include <arbor/common_types.hpp>
 #include <arbor/util/expected.hpp>
+#include <arbor/util/hash_def.hpp>
 
 #include "label_resolution.hpp"
 #include "util/partition.hpp"
-#include "util/rangeutil.hpp"
 #include "util/span.hpp"
 
 namespace arb {
@@ -17,39 +18,49 @@ namespace arb {
 cell_label_range::cell_label_range(std::vector<cell_size_type> size_vec,
                                    std::vector<cell_tag_type> label_vec,
                                    std::vector<lid_range> range_vec):
-    sizes_(std::move(size_vec)), labels_(std::move(label_vec)), ranges_(std::move(range_vec))
+    sizes(std::move(size_vec)), ranges(std::move(range_vec))
+{
+    std::transform(label_vec.begin(), label_vec.end(),
+                   std::back_inserter(labels),
+                   hash_value<const std::string&>);
+    arb_assert(check_invariant());
+};
+
+cell_label_range::cell_label_range(std::vector<cell_size_type> size_vec,
+                                   std::vector<hash_type> label_vec,
+                                   std::vector<lid_range> range_vec):
+    sizes(std::move(size_vec)), labels(std::move(label_vec)), ranges(std::move(range_vec))
 {
     arb_assert(check_invariant());
 };
 
-void cell_label_range::add_cell() {
-    sizes_.push_back(0);
-}
 
-void cell_label_range::add_label(cell_tag_type label, lid_range range) {
-    if (sizes_.empty()) throw arbor_internal_error("adding label to cell_label_range without cell");
-    ++sizes_.back();
-    labels_.push_back(std::move(label));
-    ranges_.push_back(std::move(range));
+void cell_label_range::add_cell() { sizes.push_back(0); }
+
+void cell_label_range::add_label(hash_type label, lid_range range) {
+    if (sizes.empty()) throw arbor_internal_error("adding label to cell_label_range without cell");
+    ++sizes.back();
+    labels.push_back(label);
+    ranges.push_back(std::move(range));
 }
 
 void cell_label_range::append(cell_label_range other) {
     using std::make_move_iterator;
-    sizes_.insert(sizes_.end(), make_move_iterator(other.sizes_.begin()), make_move_iterator(other.sizes_.end()));
-    labels_.insert(labels_.end(), make_move_iterator(other.labels_.begin()), make_move_iterator(other.labels_.end()));
-    ranges_.insert(ranges_.end(), make_move_iterator(other.ranges_.begin()), make_move_iterator(other.ranges_.end()));
+    sizes.insert(sizes.end(),   make_move_iterator(other.sizes.begin()),  make_move_iterator(other.sizes.end()));
+    labels.insert(labels.end(), make_move_iterator(other.labels.begin()), make_move_iterator(other.labels.end()));
+    ranges.insert(ranges.end(), make_move_iterator(other.ranges.begin()), make_move_iterator(other.ranges.end()));
 }
 
 bool cell_label_range::check_invariant() const {
-    const cell_size_type count = std::accumulate(sizes_.begin(), sizes_.end(), cell_size_type(0));
-    return count==labels_.size() && count==ranges_.size();
+    const cell_size_type count = std::accumulate(sizes.begin(), sizes.end(), cell_size_type(0));
+    return count==labels.size() && count==ranges.size();
 }
 
 // cell_labels_and_gids methods
 cell_labels_and_gids::cell_labels_and_gids(cell_label_range lr, std::vector<cell_gid_type> gid):
     label_range(std::move(lr)), gids(std::move(gid))
 {
-    if (label_range.sizes().size()!=gids.size()) throw arbor_internal_error("cell_label_range and gid count mismatch");
+    if (label_range.sizes.size()!=gids.size()) throw arbor_internal_error("cell_label_range and gid count mismatch");
 }
 
 void cell_labels_and_gids::append(cell_labels_and_gids other) {
@@ -58,7 +69,7 @@ void cell_labels_and_gids::append(cell_labels_and_gids other) {
 }
 
 bool cell_labels_and_gids::check_invariant() const {
-    return label_range.check_invariant() && label_range.sizes().size()==gids.size();
+    return label_range.check_invariant() && label_range.sizes.size()==gids.size();
 }
 
 // label_resolution_map methods
@@ -82,34 +93,35 @@ lid_hopefully label_resolution_map::range_set::at(unsigned idx) const {
 }
 
 const label_resolution_map::range_set& label_resolution_map::at(cell_gid_type gid, const cell_tag_type& tag) const {
-    return map.at(gid).at(tag);
+    return map.at(gid).at(hash_value(tag));
 }
 
 std::size_t label_resolution_map::count(cell_gid_type gid, const cell_tag_type& tag) const {
     if (!map.count(gid)) return 0u;
-    return map.at(gid).count(tag);
+    return map.at(gid).count(hash_value(tag));
 }
 
 label_resolution_map::label_resolution_map(const cell_labels_and_gids& clg) {
     arb_assert(clg.label_range.check_invariant());
     const auto& gids = clg.gids;
-    const auto& labels = clg.label_range.labels();
-    const auto& ranges = clg.label_range.ranges();
-    const auto& sizes = clg.label_range.sizes();
+    const auto& labels = clg.label_range.labels;
+    const auto& ranges = clg.label_range.ranges;
+    const auto& sizes = clg.label_range.sizes;
 
     std::vector<cell_size_type> label_divs;
     auto partn = util::make_partition(label_divs, sizes);
     for (auto i: util::count_along(partn)) {
         auto gid = gids[i];
 
-        std::unordered_map<cell_tag_type, range_set> m;
+        std::unordered_map<hash_type, range_set> m;
         for (auto label_idx: util::make_span(partn[i])) {
             const auto range = ranges[label_idx];
             auto size = int(range.end - range.begin);
             if (size < 0) {
                 throw arb::arbor_internal_error("label_resolution_map: invalid lid_range");
             }
-            auto& range_set = m[labels[label_idx]];
+            auto& label = labels[label_idx];
+            auto& range_set = m[label];
             range_set.ranges.push_back(range);
             range_set.ranges_partition.push_back(range_set.ranges_partition.back() + size);
         }
@@ -204,10 +216,12 @@ lid_hopefully update_state(resolver::state_variant& v,
 cell_lid_type resolver::resolve(cell_gid_type gid, const cell_local_label_type& label) {
     const auto& [tag, pol] = label;
 
+    auto hash = hash_value(tag);
+
     if (!label_map_->count(gid, tag)) throw arb::bad_connection_label(gid, tag, "label does not exist");
     const auto& range_set = label_map_->at(gid, tag);
 
-    auto& state = state_map_[gid][tag];
+    auto& state = state_map_[gid][hash];
 
     // Policy round_robin_halt: use previous state of round_robin policy, if existent
     if (pol == lid_selection_policy::round_robin_halt
diff --git a/arbor/label_resolution.hpp b/arbor/label_resolution.hpp
index 9b30a41fa30e6e47ba384808684b1f4c48b09ce6..6f0b14d47b9db710657e7a999670d871adc3c5aa 100644
--- a/arbor/label_resolution.hpp
+++ b/arbor/label_resolution.hpp
@@ -8,7 +8,7 @@
 #include <arbor/common_types.hpp>
 #include <arbor/util/expected.hpp>
 
-#include "util/partition.hpp"
+#include <arbor/util/hash_def.hpp>
 
 namespace arb {
 
@@ -18,37 +18,32 @@ using lid_hopefully = arb::util::expected<cell_lid_type, std::string>;
 // `sizes` is a partitioning vector for associating a cell with a set of
 // (label, range) pairs in `labels`, `ranges`.
 // gids of the cells are unknown.
-class ARB_ARBOR_API cell_label_range {
-public:
+struct ARB_ARBOR_API cell_label_range {
     cell_label_range() = default;
     cell_label_range(cell_label_range&&) = default;
     cell_label_range(const cell_label_range&) = default;
     cell_label_range& operator=(const cell_label_range&) = default;
     cell_label_range& operator=(cell_label_range&&) = default;
 
-    cell_label_range(std::vector<cell_size_type> size_vec, std::vector<cell_tag_type> label_vec, std::vector<lid_range> range_vec);
+    cell_label_range(std::vector<cell_size_type> size_vec, std::vector<cell_tag_type> label_vec, std::vector<lid_range> rapfnge_vec);
+    cell_label_range(std::vector<cell_size_type> size_vec, std::vector<hash_type> label_vec, std::vector<lid_range> range_vec);
 
     void add_cell();
 
-    void add_label(cell_tag_type label, lid_range range);
+    void add_label(hash_type label, lid_range range);
 
     void append(cell_label_range other);
 
     bool check_invariant() const;
 
-    const auto& sizes() const { return sizes_; }
-    const auto& labels() const { return labels_; }
-    const auto& ranges() const { return ranges_; }
-
-private:
     // The number of labels associated with each cell.
-    std::vector<cell_size_type> sizes_;
+    std::vector<cell_size_type> sizes;
 
     // The labels corresponding to each cell, partitioned according to sizes_.
-    std::vector<cell_tag_type> labels_;
+    std::vector<hash_type> labels;
 
     // The lid_range corresponding to each label.
-    std::vector<lid_range> ranges_;
+    std::vector<lid_range> ranges;
 };
 
 // Struct for associating each cell of `cell_label_range` with a gid.
@@ -83,7 +78,7 @@ public:
     std::size_t count(cell_gid_type gid, const cell_tag_type& tag) const;
 
 private:
-    std::unordered_map<cell_gid_type, std::unordered_map<cell_tag_type, range_set>> map;
+    std::unordered_map<cell_gid_type, std::unordered_map<hash_type, range_set>> map;
 };
 
 struct ARB_ARBOR_API round_robin_state {
@@ -123,6 +118,6 @@ private:
     state_variant construct_state(lid_selection_policy pol, cell_lid_type state);
 
     const label_resolution_map* label_map_;
-    map<cell_gid_type, map<cell_tag_type, map<lid_selection_policy, state_variant>>> state_map_;
+    map<cell_gid_type, map<hash_type, map<lid_selection_policy, state_variant>>> state_map_;
 };
 } // namespace arb
diff --git a/arbor/lif_cell_group.cpp b/arbor/lif_cell_group.cpp
index b83f006454af8ed8fba1149f162d98629b8c1feb..6698d0edefa9ea3584b2bbc0a93c5a3c0f64fed4 100644
--- a/arbor/lif_cell_group.cpp
+++ b/arbor/lif_cell_group.cpp
@@ -24,8 +24,8 @@ lif_cell_group::lif_cell_group(const std::vector<cell_gid_type>& gids,
         // tell our caller about this cell's connections
         cg_sources.add_cell();
         cg_targets.add_cell();
-        cg_sources.add_label(cell.source, {0, 1});
-        cg_targets.add_label(cell.target, {0, 1});
+        cg_sources.add_label(hash_value(cell.source), {0, 1});
+        cg_targets.add_label(hash_value(cell.target), {0, 1});
         // insert probes where needed
         auto probes = rec.get_probes(gid);
         for (const auto& probe: probes) {
diff --git a/arbor/spike_source_cell_group.cpp b/arbor/spike_source_cell_group.cpp
index 92ea2799b70cd7935bdb10ebe756fb79acc2a4a3..651980aa44cb37ca546b259f58c930f5d1e7a8e7 100644
--- a/arbor/spike_source_cell_group.cpp
+++ b/arbor/spike_source_cell_group.cpp
@@ -1,5 +1,3 @@
-#include <exception>
-
 #include <arbor/arbexcept.hpp>
 #include <arbor/recipe.hpp>
 #include <arbor/spike_source_cell.hpp>
@@ -33,7 +31,7 @@ spike_source_cell_group::spike_source_cell_group(
         try {
             auto cell = util::any_cast<spike_source_cell>(rec.get_cell_description(gid));
             time_sequences_.emplace_back(cell.seqs);
-            cg_sources.add_label(cell.source, {0, 1});
+            cg_sources.add_label(hash_value(cell.source), {0, 1});
         }
         catch (std::bad_any_cast& e) {
             throw bad_cell_description(cell_kind::spike_source, gid);
diff --git a/arborio/cableio.cpp b/arborio/cableio.cpp
index 2a15455e79955df27a9df8355a54068ff1206b62..b0383b74a922e58eacff3e3b424e40c8ac6d9f3a 100644
--- a/arborio/cableio.cpp
+++ b/arborio/cableio.cpp
@@ -135,8 +135,10 @@ s_expr mksexp(const decor& d) {
             { return slist("paint"_symbol, round_trip(p.first), mksexp(x)); }, p.second));
     }
     for (const auto& p: d.placements()) {
-        decorations.push_back(std::visit([&](auto& x)
-            { return slist("place"_symbol, round_trip(std::get<0>(p)), mksexp(x), s_expr(std::get<2>(p))); }, std::get<1>(p)));
+        decorations.push_back(std::visit([&](auto& x) {
+            auto lbl = d.tag_of(std::get<2>(p));
+            return slist("place"_symbol, round_trip(std::get<0>(p)), mksexp(x), s_expr(lbl));
+        }, std::get<1>(p)));
     }
     return {"decor"_symbol, slist_range(decorations)};
 }
@@ -282,9 +284,9 @@ decor make_decor(const std::vector<std::variant<place_tuple, paint_pair, default
     decor d;
     for(const auto& a: args) {
         auto decor_visitor = arb::util::overload(
-            [&](const place_tuple & p) { d.place(std::get<0>(p), std::get<1>(p), std::get<2>(p)); },
-            [&](const paint_pair & p) { d.paint(p.first, p.second); },
-            [&](const defaultable & p){ d.set_default(p); });
+            [&](const place_tuple& p) { d.place(std::get<0>(p), std::get<1>(p), std::get<2>(p)); },
+            [&](const paint_pair& p) { d.paint(p.first, p.second); },
+            [&](const defaultable& p){ d.set_default(p); });
         std::visit(decor_visitor, a);
     }
     return d;
diff --git a/python/cells.cpp b/python/cells.cpp
index f46bcf8aaf08948ada06f449bebeab735db6aadd..c1c446b53cddd9c5351e4ee3e96697645dde1e89 100644
--- a/python/cells.cpp
+++ b/python/cells.cpp
@@ -829,7 +829,7 @@ void register_cells(pybind11::module& m) {
             [](arb::decor& dec) {
                 std::vector<std::tuple<std::string, arb::placeable, std::string>> result;
                 for (const auto& [k, v, t]: dec.placements()) {
-                    result.emplace_back(to_string(k), v, t);
+                    result.emplace_back(to_string(k), v, dec.tag_of(t));
                 }
                 return result;
             },
diff --git a/test/unit/CMakeLists.txt b/test/unit/CMakeLists.txt
index 0438464732183ac26162fdaa62966c7fd1a76d2f..6da2140a821221396e266af5e092743d2b55b0a7 100644
--- a/test/unit/CMakeLists.txt
+++ b/test/unit/CMakeLists.txt
@@ -80,6 +80,7 @@ set(unit_sources
     test_forest.cpp
     test_fvm_layout.cpp
     test_fvm_lowered.cpp
+    test_hash.cpp
     test_gathered_vector.cpp
     test_diffusion.cpp
     test_iexpr.cpp
diff --git a/test/unit/test_cable_cell.cpp b/test/unit/test_cable_cell.cpp
index 1e59691bd291e97c5ff9c72b4324fa3cc6c7d88c..8b0a4d460dff135dd2d46dc03b801c78150b2cf0 100644
--- a/test/unit/test_cable_cell.cpp
+++ b/test/unit/test_cable_cell.cpp
@@ -1,6 +1,8 @@
 #include <gtest/gtest.h>
+
 #include "../common_cells.hpp"
 
+#include <arbor/util/hash_def.hpp>
 #include <arbor/cable_cell.hpp>
 #include <arbor/cable_cell_param.hpp>
 
@@ -45,20 +47,20 @@ TEST(cable_cell, lid_ranges) {
     const auto& src_ranges = cell.detector_ranges();
     const auto& tgt_ranges = cell.synapse_ranges();
 
-    EXPECT_EQ(1u, tgt_ranges.count("t0"));
-    EXPECT_EQ(1u, tgt_ranges.count("t1"));
-    EXPECT_EQ(1u, src_ranges.count("s0"));
-    EXPECT_EQ(1u, tgt_ranges.count("t2"));
-    EXPECT_EQ(1u, src_ranges.count("s1"));
-    EXPECT_EQ(2u, tgt_ranges.count("t3"));
+    EXPECT_EQ(1u, tgt_ranges.count(hash_value("t0")));
+    EXPECT_EQ(1u, tgt_ranges.count(hash_value("t1")));
+    EXPECT_EQ(1u, src_ranges.count(hash_value("s0")));
+    EXPECT_EQ(1u, tgt_ranges.count(hash_value("t2")));
+    EXPECT_EQ(1u, src_ranges.count(hash_value("s1")));
+    EXPECT_EQ(2u, tgt_ranges.count(hash_value("t3")));
 
-    auto r1 = tgt_ranges.equal_range("t0").first->second;
-    auto r2 = tgt_ranges.equal_range("t1").first->second;
-    auto r3 = src_ranges.equal_range("s0").first->second;
-    auto r4 = tgt_ranges.equal_range("t2").first->second;
-    auto r5 = src_ranges.equal_range("s1").first->second;
+    auto r1 = tgt_ranges.equal_range(hash_value("t0")).first->second;
+    auto r2 = tgt_ranges.equal_range(hash_value("t1")).first->second;
+    auto r3 = src_ranges.equal_range(hash_value("s0")).first->second;
+    auto r4 = tgt_ranges.equal_range(hash_value("t2")).first->second;
+    auto r5 = src_ranges.equal_range(hash_value("s1")).first->second;
 
-    auto r6_range = tgt_ranges.equal_range("t3");
+    auto r6_range = tgt_ranges.equal_range(hash_value("t3"));
     auto r6_0 = r6_range.first;
     auto r6_1 = std::next(r6_range.first);
     if (r6_0->second.begin != 4u) {
diff --git a/test/unit/test_fvm_lowered.cpp b/test/unit/test_fvm_lowered.cpp
index 19b064f60e137fac090fcf8316e925262a7dfeed..28cc19562ef0dda0e3156f90ebf6731689aaee24 100644
--- a/test/unit/test_fvm_lowered.cpp
+++ b/test/unit/test_fvm_lowered.cpp
@@ -1,5 +1,4 @@
 #include <cmath>
-#include <numeric>
 #include <string>
 #include <vector>
 
@@ -25,11 +24,8 @@
 #include "backends/multicore/fvm.hpp"
 #include "fvm_lowered_cell.hpp"
 #include "fvm_lowered_cell_impl.hpp"
-#include "util/meta.hpp"
-#include "util/maputil.hpp"
 #include "util/rangeutil.hpp"
 #include "util/span.hpp"
-#include "util/transform.hpp"
 
 #include "common.hpp"
 #include "mech_private_field_access.hpp"
@@ -993,94 +989,122 @@ TEST(fvm_lowered, label_data) {
     {
         auto clg = cell_labels_and_gids(fvm_info.target_data, gids);
         std::vector<cell_size_type> expected_sizes = {2, 0, 0, 2, 0, 0, 2, 0, 0, 2};
-        std::vector<std::pair<cell_tag_type, lid_range>> expected_labeled_ranges, actual_labeled_ranges;
-        expected_labeled_ranges = {
-            {"1_synapse",  {4, 5}}, {"4_synapses", {0, 4}},
-            {"1_synapse",  {4, 5}}, {"4_synapses", {0, 4}},
-            {"1_synapse",  {4, 5}}, {"4_synapses", {0, 4}},
-            {"1_synapse",  {4, 5}}, {"4_synapses", {0, 4}}
+        std::vector<std::pair<hash_type, lid_range>> expected_labeled_ranges = {
+            {hash_value("1_synapse"),  {4, 5}}, {hash_value("4_synapses"), {0, 4}},
+            {hash_value("1_synapse"),  {4, 5}}, {hash_value("4_synapses"), {0, 4}},
+            {hash_value("1_synapse"),  {4, 5}}, {hash_value("4_synapses"), {0, 4}},
+            {hash_value("1_synapse"),  {4, 5}}, {hash_value("4_synapses"), {0, 4}}
         };
 
+        std::vector<std::pair<hash_type, lid_range>> actual_labeled_ranges;
+
         EXPECT_EQ(clg.gids, gids);
-        EXPECT_EQ(clg.label_range.sizes(), expected_sizes);
-        EXPECT_EQ(clg.label_range.labels().size(), expected_labeled_ranges.size());
-        EXPECT_EQ(clg.label_range.ranges().size(), expected_labeled_ranges.size());
+        EXPECT_EQ(clg.label_range.sizes, expected_sizes);
+        EXPECT_EQ(clg.label_range.labels.size(), expected_labeled_ranges.size());
+        EXPECT_EQ(clg.label_range.ranges.size(), expected_labeled_ranges.size());
 
         for (unsigned i = 0; i < expected_labeled_ranges.size(); ++i) {
-            actual_labeled_ranges.push_back({clg.label_range.labels()[i], clg.label_range.ranges()[i]});
+            actual_labeled_ranges.push_back({clg.label_range.labels[i], clg.label_range.ranges[i]});
         }
 
         std::vector<cell_size_type> size_partition;
         auto part = util::make_partition(size_partition, expected_sizes);
         for (const auto& r: part) {
             util::sort(util::subrange_view(actual_labeled_ranges, r));
+            util::sort(util::subrange_view(expected_labeled_ranges, r));
         }
         EXPECT_EQ(actual_labeled_ranges, expected_labeled_ranges);
+
+        // Check for hash collisions; if we have one, the hash will appear twice in a given range,
+        // making the set of ids smaller than expected
+        const auto& labels = clg.label_range.labels;
+        for (const auto& [beg, end]: part) {
+            std::unordered_set<hash_type> unique(labels.begin() + beg, labels.begin() + end);
+            EXPECT_EQ(unique.size(), end - beg);
+        }
     }
 
     // detectors
     {
         auto clg = cell_labels_and_gids(fvm_info.source_data, gids);
         std::vector<cell_size_type> expected_sizes = {1, 2, 2, 1, 2, 2, 1, 2, 2, 1};
-        std::vector<std::pair<cell_tag_type, lid_range>> expected_labeled_ranges, actual_labeled_ranges;
-        expected_labeled_ranges = {
-            {"1_detector",  {0, 1}},
-            {"2_detectors", {3, 5}}, {"3_detectors", {0, 3}},
-            {"2_detectors", {3, 5}}, {"3_detectors", {0, 3}},
-            {"1_detector",  {0, 1}},
-            {"2_detectors", {3, 5}}, {"3_detectors", {0, 3}},
-            {"2_detectors", {3, 5}}, {"3_detectors", {0, 3}},
-            {"1_detector",  {0, 1}},
-            {"2_detectors", {3, 5}}, {"3_detectors", {0, 3}},
-            {"2_detectors", {3, 5}}, {"3_detectors", {0, 3}},
-            {"1_detector",  {0, 1}}
+        std::vector<std::pair<hash_type, lid_range>> expected_labeled_ranges = {
+            {hash_value("1_detector"),  {0, 1}},
+            {hash_value("2_detectors"), {3, 5}}, {hash_value("3_detectors"), {0, 3}},
+            {hash_value("2_detectors"), {3, 5}}, {hash_value("3_detectors"), {0, 3}},
+            {hash_value("1_detector"),  {0, 1}},
+            {hash_value("2_detectors"), {3, 5}}, {hash_value("3_detectors"), {0, 3}},
+            {hash_value("2_detectors"), {3, 5}}, {hash_value("3_detectors"), {0, 3}},
+            {hash_value("1_detector"),  {0, 1}},
+            {hash_value("2_detectors"), {3, 5}}, {hash_value("3_detectors"), {0, 3}},
+            {hash_value("2_detectors"), {3, 5}}, {hash_value("3_detectors"), {0, 3}},
+            {hash_value("1_detector"),  {0, 1}}
         };
+        std::vector<std::pair<hash_type, lid_range>> actual_labeled_ranges;
 
         EXPECT_EQ(clg.gids, gids);
-        EXPECT_EQ(clg.label_range.sizes(), expected_sizes);
-        EXPECT_EQ(clg.label_range.labels().size(), expected_labeled_ranges.size());
-        EXPECT_EQ(clg.label_range.ranges().size(), expected_labeled_ranges.size());
+        EXPECT_EQ(clg.label_range.sizes, expected_sizes);
+        EXPECT_EQ(clg.label_range.labels.size(), expected_labeled_ranges.size());
+        EXPECT_EQ(clg.label_range.ranges.size(), expected_labeled_ranges.size());
 
         for (unsigned i = 0; i < expected_labeled_ranges.size(); ++i) {
-            actual_labeled_ranges.push_back({clg.label_range.labels()[i], clg.label_range.ranges()[i]});
+            actual_labeled_ranges.push_back({clg.label_range.labels[i], clg.label_range.ranges[i]});
         }
 
         std::vector<cell_size_type> size_partition;
         auto part = util::make_partition(size_partition, expected_sizes);
         for (const auto& r: part) {
             util::sort(util::subrange_view(actual_labeled_ranges, r));
+            util::sort(util::subrange_view(expected_labeled_ranges, r));
         }
         EXPECT_EQ(actual_labeled_ranges, expected_labeled_ranges);
+
+        // Check for hash collisions; if we have one, the hash will appear twice in a given range,
+        // making the set of ids smaller than expected
+        const auto& labels = clg.label_range.labels;
+        for (const auto& [beg, end]: part) {
+            std::unordered_set<hash_type> unique(labels.begin() + beg, labels.begin() + end);
+            EXPECT_EQ(unique.size(), end - beg);
+        }
     }
 
     // gap_junctions
     {
         auto clg = cell_labels_and_gids(fvm_info.gap_junction_data, gids);
         std::vector<cell_size_type> expected_sizes = {0, 2, 2, 0, 2, 2, 0, 2, 2, 0};
-        std::vector<std::pair<cell_tag_type, lid_range>> expected_labeled_ranges, actual_labeled_ranges;
-        expected_labeled_ranges = {
-            {"1_gap_junction",  {2, 3}}, {"2_gap_junctions", {0, 2}},
-            {"1_gap_junction",  {2, 3}}, {"2_gap_junctions", {0, 2}},
-            {"1_gap_junction",  {2, 3}}, {"2_gap_junctions", {0, 2}},
-            {"1_gap_junction",  {2, 3}}, {"2_gap_junctions", {0, 2}},
-            {"1_gap_junction",  {2, 3}}, {"2_gap_junctions", {0, 2}},
-            {"1_gap_junction",  {2, 3}}, {"2_gap_junctions", {0, 2}},
+        std::vector<std::pair<hash_type, lid_range>> expected_labeled_ranges = {
+            {hash_value("1_gap_junction"),  {2, 3}}, {hash_value("2_gap_junctions"), {0, 2}},
+            {hash_value("1_gap_junction"),  {2, 3}}, {hash_value("2_gap_junctions"), {0, 2}},
+            {hash_value("1_gap_junction"),  {2, 3}}, {hash_value("2_gap_junctions"), {0, 2}},
+            {hash_value("1_gap_junction"),  {2, 3}}, {hash_value("2_gap_junctions"), {0, 2}},
+            {hash_value("1_gap_junction"),  {2, 3}}, {hash_value("2_gap_junctions"), {0, 2}},
+            {hash_value("1_gap_junction"),  {2, 3}}, {hash_value("2_gap_junctions"), {0, 2}},
         };
 
         EXPECT_EQ(clg.gids, gids);
-        EXPECT_EQ(clg.label_range.sizes(), expected_sizes);
-        EXPECT_EQ(clg.label_range.labels().size(), expected_labeled_ranges.size());
-        EXPECT_EQ(clg.label_range.ranges().size(), expected_labeled_ranges.size());
+        EXPECT_EQ(clg.label_range.sizes, expected_sizes);
+        EXPECT_EQ(clg.label_range.labels.size(), expected_labeled_ranges.size());
+        EXPECT_EQ(clg.label_range.ranges.size(), expected_labeled_ranges.size());
 
+        std::vector<std::pair<hash_type, lid_range>> actual_labeled_ranges;
         for (unsigned i = 0; i < expected_labeled_ranges.size(); ++i) {
-            actual_labeled_ranges.push_back({clg.label_range.labels()[i], clg.label_range.ranges()[i]});
+            actual_labeled_ranges.push_back({clg.label_range.labels[i], clg.label_range.ranges[i]});
         }
 
         std::vector<cell_size_type> size_partition;
         auto part = util::make_partition(size_partition, expected_sizes);
         for (const auto& r: part) {
             util::sort(util::subrange_view(actual_labeled_ranges, r));
+            util::sort(util::subrange_view(expected_labeled_ranges, r));
         }
         EXPECT_EQ(actual_labeled_ranges, expected_labeled_ranges);
+
+        // Check for hash collisions; if we have one, the hash will appear twice in a given range,
+        // making the set of ids smaller than expected
+        const auto& labels = clg.label_range.labels;
+        for (const auto& [beg, end]: part) {
+            std::unordered_set<hash_type> unique(labels.begin() + beg, labels.begin() + end);
+            EXPECT_EQ(unique.size(), end - beg);
+        }
     }
 }
diff --git a/test/unit/test_hash.cpp b/test/unit/test_hash.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..bcab44a6d4c91701db698427cb2e49206fdafc94
--- /dev/null
+++ b/test/unit/test_hash.cpp
@@ -0,0 +1,24 @@
+#include <gtest/gtest.h>
+
+#include <string>
+
+#include <arbor/util/hash_def.hpp>
+
+TEST(hash, string_eq) {
+    ASSERT_EQ(arb::hash_value("foobar"), arb::hash_value(std::string{"foobar"}));
+    ASSERT_EQ(arb::hash_value("foobar"), arb::hash_value("foobar"));
+    ASSERT_NE(arb::hash_value("foobar"), arb::hash_value("barfoo"));
+}
+
+TEST(hash, doesnt_compile) {
+    double foo = 42;
+    // Sadly we cannot check static assertions... this shoudln't compile
+    // EXPECT_ANY_THROW(arb::hash_value(&foo));
+    // this should
+    arb::hash_value((void*) &foo);
+}
+
+// check that we do not fall into the trap of the STL...
+TEST(hash, integral_is_not_identity) {
+    ASSERT_NE(arb::hash_value(42), 42);
+}
diff --git a/test/unit/test_label_resolution.cpp b/test/unit/test_label_resolution.cpp
index 75adfbdae29104055c437c4673301475c87597b9..32c262ee3d9086da5d64bdf20381d3c9660feda1 100644
--- a/test/unit/test_label_resolution.cpp
+++ b/test/unit/test_label_resolution.cpp
@@ -8,6 +8,14 @@
 
 using namespace arb;
 
+std::vector<hash_type> make_labels(const std::vector<std::string>& ls) {
+    std::vector<hash_type> res;
+    std::transform(ls.begin(), ls.end(),
+                   std::back_inserter(res),
+                   hash_value<const std::string&>);
+    return res;
+}
+
 TEST(test_cell_label_range, build) {
     using ivec = std::vector<cell_size_type>;
     using svec = std::vector<cell_tag_type>;
@@ -15,65 +23,66 @@ TEST(test_cell_label_range, build) {
 
     // Test add_cell and add_label
     auto b0 = cell_label_range();
-    EXPECT_THROW(b0.add_label("l0", {0u, 1u}), arb::arbor_internal_error);
-    EXPECT_TRUE(b0.sizes().empty());
-    EXPECT_TRUE(b0.labels().empty());
-    EXPECT_TRUE(b0.ranges().empty());
+    EXPECT_THROW(b0.add_label(hash_value("l0"), {0u, 1u}), arb::arbor_internal_error);
+    EXPECT_TRUE(b0.sizes.empty());
+    EXPECT_TRUE(b0.labels.empty());
+    EXPECT_TRUE(b0.ranges.empty());
     EXPECT_TRUE(b0.check_invariant());
 
     auto b1 = cell_label_range();
     b1.add_cell();
     b1.add_cell();
     b1.add_cell();
-    EXPECT_EQ((ivec{0u, 0u, 0u}), b1.sizes());
-    EXPECT_TRUE(b1.labels().empty());
-    EXPECT_TRUE(b1.ranges().empty());
+    EXPECT_EQ((ivec{0u, 0u, 0u}), b1.sizes);
+    EXPECT_TRUE(b1.labels.empty());
+    EXPECT_TRUE(b1.ranges.empty());
     EXPECT_TRUE(b1.check_invariant());
 
     auto b2 = cell_label_range();
     b2.add_cell();
-    b2.add_label("l0", {0u, 1u});
-    b2.add_label("l0", {3u, 13u});
-    b2.add_label("l1", {0u, 5u});
+    b2.add_label(hash_value("l0"), {0u, 1u});
+    b2.add_label(hash_value("l0"), {3u, 13u});
+    b2.add_label(hash_value("l1"), {0u, 5u});
     b2.add_cell();
     b2.add_cell();
-    b2.add_label("l2", {6u, 8u});
-    b2.add_label("l3", {1u, 0u});
-    b2.add_label("l4", {7u, 2u});
-    b2.add_label("l4", {7u, 2u});
-    b2.add_label("l2", {7u, 2u});
-    EXPECT_EQ((ivec{3u, 0u, 5u}), b2.sizes());
-    EXPECT_EQ((svec{"l0", "l0", "l1", "l2", "l3", "l4", "l4", "l2"}), b2.labels());
-    EXPECT_EQ((lvec{{0u, 1u}, {3u, 13u}, {0u, 5u}, {6u, 8u}, {1u, 0u}, {7u, 2u}, {7u, 2u}, {7u, 2u}}), b2.ranges());
+    b2.add_label(hash_value("l2"), {6u, 8u});
+    b2.add_label(hash_value("l3"), {1u, 0u});
+    b2.add_label(hash_value("l4"), {7u, 2u});
+    b2.add_label(hash_value("l4"), {7u, 2u});
+    b2.add_label(hash_value("l2"), {7u, 2u});
+    EXPECT_EQ((ivec{3u, 0u, 5u}), b2.sizes);
+    EXPECT_EQ(make_labels(svec{"l0", "l0", "l1", "l2", "l3", "l4", "l4", "l2"}), b2.labels);
+    EXPECT_EQ((lvec{{0u, 1u}, {3u, 13u}, {0u, 5u}, {6u, 8u}, {1u, 0u}, {7u, 2u}, {7u, 2u}, {7u, 2u}}), b2.ranges);
     EXPECT_TRUE(b2.check_invariant());
 
     auto b3 = cell_label_range();
     b3.add_cell();
-    b3.add_label("r0", {0u, 9u});
-    b3.add_label("r1", {10u, 10u});
+    b3.add_label(hash_value("r0"), {0u, 9u});
+    b3.add_label(hash_value("r1"), {10u, 10u});
     b3.add_cell();
-    EXPECT_EQ((ivec{2u, 0u}), b3.sizes());
-    EXPECT_EQ((svec{"r0", "r1"}), b3.labels());
-    EXPECT_EQ((lvec{{0u, 9u}, {10u, 10u}}), b3.ranges());
+    EXPECT_EQ((ivec{2u, 0u}), b3.sizes);
+    EXPECT_EQ(make_labels
+              (svec{"r0", "r1"}), b3.labels);
+    EXPECT_EQ((lvec{{0u, 9u}, {10u, 10u}}), b3.ranges);
     EXPECT_TRUE(b3.check_invariant());
 
     // Test appending
     b0.append(b1);
-    EXPECT_EQ((ivec{0u, 0u, 0u}), b0.sizes());
-    EXPECT_TRUE(b0.labels().empty());
-    EXPECT_TRUE(b0.ranges().empty());
+    EXPECT_EQ((ivec{0u, 0u, 0u}), b0.sizes);
+    EXPECT_TRUE(b0.labels.empty());
+    EXPECT_TRUE(b0.ranges.empty());
     EXPECT_TRUE(b0.check_invariant());
 
     b0.append(b2);
-    EXPECT_EQ((ivec{0u, 0u, 0u, 3u, 0u, 5u}), b0.sizes());
-    EXPECT_EQ((svec{"l0", "l0", "l1", "l2", "l3", "l4", "l4", "l2"}), b0.labels());
-    EXPECT_EQ((lvec{{0u, 1u}, {3u, 13u}, {0u, 5u}, {6u, 8u}, {1u, 0u}, {7u, 2u}, {7u, 2u}, {7u, 2u}}), b0.ranges());
+    EXPECT_EQ((ivec{0u, 0u, 0u, 3u, 0u, 5u}), b0.sizes);
+    EXPECT_EQ(make_labels(svec{"l0", "l0", "l1", "l2", "l3", "l4", "l4", "l2"}), b0.labels);
+    EXPECT_EQ((lvec{{0u, 1u}, {3u, 13u}, {0u, 5u}, {6u, 8u}, {1u, 0u}, {7u, 2u}, {7u, 2u}, {7u, 2u}}), b0.ranges);
     EXPECT_TRUE(b0.check_invariant());
 
     b0.append(b3);
-    EXPECT_EQ((ivec{0u, 0u, 0u, 3u, 0u, 5u, 2u, 0u}), b0.sizes());
-    EXPECT_EQ((svec{"l0", "l0", "l1", "l2", "l3", "l4", "l4", "l2", "r0", "r1"}), b0.labels());
-    EXPECT_EQ((lvec{{0u, 1u}, {3u, 13u}, {0u, 5u}, {6u, 8u}, {1u, 0u}, {7u, 2u}, {7u, 2u}, {7u, 2u}, {0u, 9u}, {10u, 10u}}), b0.ranges());
+    EXPECT_EQ((ivec{0u, 0u, 0u, 3u, 0u, 5u, 2u, 0u}), b0.sizes);
+    EXPECT_EQ(make_labels(svec{"l0", "l0", "l1", "l2", "l3", "l4", "l4", "l2", "r0", "r1"}), b0.labels);
+    EXPECT_EQ((lvec{{0u, 1u}, {3u, 13u}, {0u, 5u}, {6u, 8u}, {1u, 0u}, {7u, 2u}, {7u, 2u}, {7u, 2u}, {0u, 9u}, {10u, 10u}}), b0.ranges);
     EXPECT_TRUE(b0.check_invariant());
 }