Open pkhuong opened 3 years ago
Did you have something like this (naive impl) in mind?
diff --git a/src/magazine.rs b/src/magazine.rs
index 63c73c6..e99ba3e 100644
--- a/src/magazine.rs
+++ b/src/magazine.rs
@@ -398,7 +398,20 @@ impl crate::class::ClassInfo {
if mag.is_empty() {
self.rack.release_empty_magazine(mag);
- } else if mag.is_full() {
+ return;
+ }
+
+ let mut set = std::collections::HashSet::new();
+ for i in 0..mag.0.len() {
+ if let Some(alloc) = mag.0.nth(i) {
+ let unique = set.insert(alloc.get().as_ptr());
+ if !unique {
+ panic!("double free of {:p}", alloc.get().as_ptr());
+ }
+ }
+ }
+
+ if mag.is_full() {
self.full_mags.push(mag);
} else {
self.partial_mags.push(mag);
This catches the following double free:
use slitter::{Class, ClassConfig};
use std::alloc::Layout;
struct C {
a: i16,
b: i32,
}
fn main() {
let class_allocator = Class::new(ClassConfig {
name: None,
layout: Layout::new::<C>(),
zero_init: true,
mapper_name: None,
})
.unwrap();
let alloc = class_allocator.allocate().unwrap();
let double_free = alloc;
class_allocator.release(alloc);
class_allocator.release(double_free);
}
thread 'main' panicked at 'double free of 0x7f1f80000000', /home/max/projects/slitter/src/magazine.rs:409:21
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
fatal runtime error: failed to initiate panic, error 5
Aborted (core dumped)
When check-contracts
is enabled this is caught already:
thread 'main' panicked at 'Pre-condition of release violated: Released blocks must match the class and not double-free.: debug_allocation_map :: mark_released(self, & block).is_ok()', /home/max/projects/slitter/src/individual.rs:54:16
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
Yes, definitely something like that! I think ClassInfo::clear_magazine
is a better place, since we're mostly concerned with catching user errors and not implementation bugs. We could however reuse the same logic in contracts, around ClassInfo::release_magazine
.
The dup-checking logic has to be efficient. I think we'll want to have that in a separate file to make it easier to test and benchmark. Magazines can't hold more than 30 pointers, so we should be able to do something nice. A vectorised sort might work well, for example? @damageboy might have some ideas?
@pervognsen reports ~4.5 cycles/element to clear a small bitmap and populate it with indexes computed with multiply-shift hash (https://en.wikipedia.org/wiki/Universal_hashing#Avoiding_modular_arithmetic). That sounds reasonable to me, and doesn't involve anything exotic or unsafe.
Of course, there could still be false positive, but a 1024-2048 bit map should have rare enough collisions.
And I guess if there's a collision we could then fall back to a slower, fully accurate method to ensure there's no false positives overall.
Yeah, my mock-up used 2048 bits and fell back to a nested-loop slow path. But I did the math and the false positive rate with 30 elements and 2048 slots is probably still too high at around 20%. But you can fix that by running the same code again with an independent hash seed. The false positive probability stacks multiplicatively, so the chance of getting a false positive a second time is only 4%. And given that the slow path is still pretty darn fast, hitting it 4% is probably fine.
Yeah, the orders of magnitude above make sense to me. We can compute multiple hashes with the same multiply-shift, although low-order bits are of lesser quality; I think it makes more sense to unconditionally perform 2-3 bitmap checks at once?
Is there a potential issue here because allocations in a loop will be close together? Barring an issue with my quick implementation here, nearby allocations all get hashed with multiply-shift to the same value:
use slitter::{Class, ClassConfig};
use std::alloc::Layout;
struct C {
a: i16,
b: i32,
}
/// Hash `x` with seed `a` using multiply-shift to (2^11) 2048 bins.
fn hash(x: usize, a: usize) -> usize {
a.wrapping_mul(x) >> (usize::BITS - 11)
}
fn main() {
let class_allocator = Class::new(ClassConfig {
name: None,
layout: Layout::new::<C>(),
zero_init: true,
mapper_name: None,
})
.unwrap();
let seed = 1265; // 2^11 = 2048 / golden ratio ~= 1265
for _ in 0..30 {
let alloc = class_allocator.allocate().unwrap();
println!("allocated {:p}, hashed to {:?}", alloc, hash(alloc.as_ptr() as usize, seed));
}
}
allocated 0x7f2b80000000, hashed to 19
allocated 0x7f2b800000f0, hashed to 19
allocated 0x7f2b800000e8, hashed to 19
...
allocated 0x7f2b80000010, hashed to 19
The multiplier is too small: it should be a pseudo-random odd usize value. If you want to use phi, you have to divide 2^64, or multiply by 2^64 and take the low bits. FWIW, I think it makes more sense to let the multiplier vary at runtime: we could regenerate it whenever we find a false positive.
We probably can't afford to check for double frees in the fast path, but a batch dup check when we flush a full magazine might be doable?