Skip to content

Commit 1a3150b

Browse files
committed
Replace internal maps with unsorted Vecs
HashMap and BTreeMap are overkill in this context. Unsorted vectors are plenty fast enough and the necessary collection interfaces are straightforward to implement. This change has two benefits. First, it improves binary size. For the `print` example from signal-hook in release mode, the .text section shrinks by about 18 KiB and overall file size shrinks by about 30 KiB. That's roughly a 6% reduction in both metrics. Second, the simpler data structures make it more obvious that the signal handler only does async-signal-safe operations. In particular, the default HashMap has a `RandomState`, which can access TLS, do dlsym lookups, open and read from files, etc. depending on the platform. I don't think that's a problem for the hash table *lookup* done in the signal handler since that shouldn't touch the `RandomState`, but it's a bit subtle and the standard library doesn't make any guarantees about this. Avoiding hash maps entirely removes the need to think about it. Performance notes: * (Un-)registering actions does an insert/remove by ActionId, which is asymptotically slower with this PR. However, (un-)registering is a slow operation and should be done rarely. Besides locking, it always clones the entire `SignalData`, so it already takes O(n) time when there's n actions registered across all symbols. * The signal handler looks up the `Slot` by signal number, which is asymptotically slower with this PR. However, there's only a very small constant number of signals, so asymptotics don't matter. * After looking up the right `Slot`, the signal handler only iterates sequentially over the actions, it doesn't do any lookups by ActionId. * For a simple microbenchmark that registers one action each for 20 signals and then raises one signal 100k times, this implementation appears to be slightly *faster* regardless of which signal is raised.
1 parent ee9a032 commit 1a3150b

File tree

2 files changed

+106
-8
lines changed

2 files changed

+106
-8
lines changed

signal-hook-registry/src/lib.rs

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -65,9 +65,8 @@
6565
extern crate libc;
6666

6767
mod half_lock;
68+
mod vec_map;
6869

69-
use std::collections::hash_map::Entry;
70-
use std::collections::{BTreeMap, HashMap};
7170
use std::io::Error;
7271
use std::mem;
7372
use std::ptr;
@@ -89,6 +88,7 @@ use libc::{SIGFPE, SIGILL, SIGKILL, SIGSEGV, SIGSTOP};
8988
use libc::{SIGFPE, SIGILL, SIGSEGV};
9089

9190
use half_lock::HalfLock;
91+
use vec_map::{Entry, VecMap};
9292

9393
// These constants are not defined in the current version of libc, but it actually
9494
// exists in Windows CRT.
@@ -140,9 +140,8 @@ type Action = Fn(&siginfo_t) + Send + Sync;
140140
#[derive(Clone)]
141141
struct Slot {
142142
prev: Prev,
143-
// We use BTreeMap here, because we want to run the actions in the order they were inserted.
144-
// This works, because the ActionIds are assigned in an increasing order.
145-
actions: BTreeMap<ActionId, Arc<Action>>,
143+
// Actions are stored and executed in the order they were registered.
144+
actions: VecMap<ActionId, Arc<Action>>,
146145
}
147146

148147
impl Slot {
@@ -200,14 +199,14 @@ impl Slot {
200199
}
201200
Ok(Slot {
202201
prev: Prev { signal, info: old },
203-
actions: BTreeMap::new(),
202+
actions: VecMap::new(),
204203
})
205204
}
206205
}
207206

208207
#[derive(Clone)]
209208
struct SignalData {
210-
signals: HashMap<c_int, Slot>,
209+
signals: VecMap<c_int, Slot>,
211210
next_id: u128,
212211
}
213212

@@ -321,7 +320,7 @@ impl GlobalData {
321320
GLOBAL_INIT.call_once(|| {
322321
let data = Box::into_raw(Box::new(GlobalData {
323322
data: HalfLock::new(SignalData {
324-
signals: HashMap::new(),
323+
signals: VecMap::new(),
325324
next_id: 1,
326325
}),
327326
race_fallback: HalfLock::new(None),
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
use std::mem;
2+
3+
/// A small map backed by an unsorted vector.
4+
///
5+
/// Maintains key uniqueness at cost of O(n) lookup/insert/remove. Maintains insertion order
6+
/// (`insert` calls that overwrite an existing value don't change order).
7+
#[derive(Clone, Default)]
8+
pub struct VecMap<K, V>(Vec<(K, V)>);
9+
10+
impl<K: Eq, V> VecMap<K, V> {
11+
pub fn new() -> Self {
12+
VecMap(Vec::new())
13+
}
14+
15+
pub fn is_empty(&self) -> bool {
16+
self.0.is_empty()
17+
}
18+
19+
pub fn clear(&mut self) {
20+
self.0.clear();
21+
}
22+
23+
fn find(&self, key: &K) -> Option<usize> {
24+
for (i, (k, _)) in self.0.iter().enumerate() {
25+
if k == key {
26+
return Some(i);
27+
}
28+
}
29+
None
30+
}
31+
32+
pub fn get(&self, key: &K) -> Option<&V> {
33+
match self.find(key) {
34+
Some(i) => Some(&self.0[i].1),
35+
None => None,
36+
}
37+
}
38+
39+
pub fn get_mut(&mut self, key: &K) -> Option<&mut V> {
40+
match self.find(key) {
41+
Some(i) => Some(&mut self.0[i].1),
42+
None => None,
43+
}
44+
}
45+
46+
pub fn insert(&mut self, key: K, value: V) -> Option<V> {
47+
if let Some(old) = self.get_mut(&key) {
48+
return Some(mem::replace(old, value));
49+
}
50+
self.0.push((key, value));
51+
None
52+
}
53+
54+
pub fn remove(&mut self, key: &K) -> Option<V> {
55+
match self.find(key) {
56+
Some(i) => Some(self.0.remove(i).1),
57+
None => None,
58+
}
59+
}
60+
61+
pub fn entry(&mut self, key: K) -> Entry<'_, K, V> {
62+
match self.find(&key) {
63+
Some(i) => Entry::Occupied(OccupiedEntry {
64+
place: &mut self.0[i],
65+
}),
66+
None => Entry::Vacant(VacantEntry { map: self, key }),
67+
}
68+
}
69+
70+
pub fn values(&self) -> impl Iterator<Item = &V> {
71+
self.0.iter().map(|kv| &kv.1)
72+
}
73+
}
74+
75+
pub enum Entry<'a, K: 'a, V: 'a> {
76+
Occupied(OccupiedEntry<'a, K, V>),
77+
Vacant(VacantEntry<'a, K, V>),
78+
}
79+
80+
pub struct OccupiedEntry<'a, K: 'a, V: 'a> {
81+
place: &'a mut (K, V),
82+
}
83+
84+
impl<'a, K, V> OccupiedEntry<'a, K, V> {
85+
pub fn get_mut(&mut self) -> &mut V {
86+
&mut self.place.1
87+
}
88+
}
89+
90+
pub struct VacantEntry<'a, K: 'a, V: 'a> {
91+
map: &'a mut VecMap<K, V>,
92+
key: K,
93+
}
94+
95+
impl<'a, K, V> VacantEntry<'a, K, V> {
96+
pub fn insert(self, value: V) {
97+
self.map.0.push((self.key, value));
98+
}
99+
}

0 commit comments

Comments
 (0)