Skip to content

Commit

Permalink
Merge pull request #7448 from roc-lang/simplify-refcount
Browse files Browse the repository at this point in the history
[Breaking Change] Simplify Refcounting
  • Loading branch information
bhansconnect authored Jan 11, 2025
2 parents 1bb9f7f + ec8aeaa commit 10ea93e
Show file tree
Hide file tree
Showing 12 changed files with 70 additions and 140 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ pub export fn main() u8 {
var raw_numbers: [NUM_NUMS + 1]i64 = undefined;

// set refcount to one
raw_numbers[0] = -9223372036854775808;
raw_numbers[0] = 1;

var numbers = raw_numbers[1..];

Expand Down
10 changes: 3 additions & 7 deletions crates/compiler/builtins/bitcode/src/list.zig
Original file line number Diff line number Diff line change
Expand Up @@ -179,23 +179,19 @@ pub const RocList = extern struct {
}

pub fn isUnique(self: RocList) bool {
return self.refcountMachine() == utils.REFCOUNT_ONE;
return utils.rcUnique(@bitCast(self.refcount()));
}

fn refcountMachine(self: RocList) usize {
fn refcount(self: RocList) usize {
if (self.getCapacity() == 0 and !self.isSeamlessSlice()) {
// the zero-capacity is Clone, copying it will not leak memory
return utils.REFCOUNT_ONE;
return 1;
}

const ptr: [*]usize = @as([*]usize, @ptrCast(@alignCast(self.getAllocationDataPtr())));
return (ptr - 1)[0];
}

fn refcountHuman(self: RocList) usize {
return self.refcountMachine() - utils.REFCOUNT_ONE + 1;
}

pub fn makeUniqueExtra(self: RocList, alignment: u32, element_width: usize, elements_refcounted: bool, dec: Dec, update_mode: UpdateMode) RocList {
if (update_mode == .InPlace) {
return self;
Expand Down
10 changes: 3 additions & 7 deletions crates/compiler/builtins/bitcode/src/str.zig
Original file line number Diff line number Diff line change
Expand Up @@ -366,22 +366,18 @@ pub const RocStr = extern struct {
}

fn isRefcountOne(self: RocStr) bool {
return self.refcountMachine() == utils.REFCOUNT_ONE;
return utils.rcUnique(@bitCast(self.refcount()));
}

fn refcountMachine(self: RocStr) usize {
fn refcount(self: RocStr) usize {
if ((self.getCapacity() == 0 and !self.isSeamlessSlice()) or self.isSmallStr()) {
return utils.REFCOUNT_ONE;
return 1;
}

const ptr: [*]usize = @as([*]usize, @ptrCast(@alignCast(self.bytes)));
return (ptr - 1)[0];
}

fn refcountHuman(self: RocStr) usize {
return self.refcountMachine() - utils.REFCOUNT_ONE + 1;
}

pub fn asSlice(self: *const RocStr) []const u8 {
return self.asU8ptr()[0..self.len()];
}
Expand Down
73 changes: 44 additions & 29 deletions crates/compiler/builtins/bitcode/src/utils.zig
Original file line number Diff line number Diff line change
Expand Up @@ -179,11 +179,10 @@ pub const IncN = fn (?[*]u8, u64) callconv(.C) void;
pub const Dec = fn (?[*]u8) callconv(.C) void;

const REFCOUNT_MAX_ISIZE: isize = 0;
pub const REFCOUNT_ONE_ISIZE: isize = std.math.minInt(isize);
pub const REFCOUNT_ONE: usize = @as(usize, @bitCast(REFCOUNT_ONE_ISIZE));
// Only top bit set.
pub const REFCOUNT_IS_NOT_ATOMIC_MASK: isize = REFCOUNT_ONE_ISIZE;
pub const REFCOUNT_ONE_ATOMIC_ISIZE: isize = 1;
const REFCOUNT_IS_ATOMIC_MASK: isize = std.math.minInt(isize);
// All other bits of the refcount.
const REFCOUNT_VALUE_MASK = ~REFCOUNT_IS_ATOMIC_MASK;

pub const IntWidth = enum(u8) {
U8 = 0,
Expand Down Expand Up @@ -215,7 +214,7 @@ pub fn increfRcPtrC(ptr_to_refcount: *isize, amount: isize) callconv(.C) void {

// Ensure that the refcount is not whole program lifetime.
const refcount: isize = ptr_to_refcount.*;
if (refcount != REFCOUNT_MAX_ISIZE) {
if (!rcConstant(refcount)) {
// Note: we assume that a refcount will never overflow.
// As such, we do not need to cap incrementing.
switch (RC_TYPE) {
Expand All @@ -227,15 +226,14 @@ pub fn increfRcPtrC(ptr_to_refcount: *isize, amount: isize) callconv(.C) void {
std.debug.print("{} + {} = {}!\n", .{ old, amount, new });
}

ptr_to_refcount.* = refcount + amount;
ptr_to_refcount.* = refcount +% amount;
},
.atomic => {
// If the first bit of the refcount is set, this variable is threadlocal.
// Use normal refcounting instead of atomic.
if (refcount & REFCOUNT_IS_NOT_ATOMIC_MASK != 0) {
ptr_to_refcount.* = refcount + amount;
} else {
// If the first bit of the refcount is set, this variable is atomic.
if (refcount & REFCOUNT_IS_ATOMIC_MASK != 0) {
_ = @atomicRmw(isize, ptr_to_refcount, .Add, amount, .monotonic);
} else {
ptr_to_refcount.* = refcount +% amount;
}
},
.none => unreachable,
Expand Down Expand Up @@ -379,32 +377,31 @@ inline fn decref_ptr_to_refcount(

// Ensure that the refcount is not whole program lifetime.
const refcount: isize = refcount_ptr[0];
if (refcount != REFCOUNT_MAX_ISIZE) {
if (!rcConstant(refcount)) {
switch (RC_TYPE) {
.normal => {
const old = @as(usize, @bitCast(refcount));
refcount_ptr[0] = refcount -% 1;
const new = @as(usize, @bitCast(refcount -% 1));

if (DEBUG_INCDEC and builtin.target.cpu.arch != .wasm32) {
const old = @as(usize, @bitCast(refcount));
const new = @as(usize, @bitCast(refcount_ptr[0] -% 1));

std.debug.print("{} - 1 = {}!\n", .{ old, new });
}

if (refcount == REFCOUNT_ONE_ISIZE) {
refcount_ptr[0] = refcount -% 1;
if (refcount == 1) {
free_ptr_to_refcount(refcount_ptr, alignment, elements_refcounted);
}
},
.atomic => {
// If the first bit of the refcount is set, this variable is threadlocal.
// Use normal refcounting instead of atomic.
if (refcount & REFCOUNT_IS_NOT_ATOMIC_MASK != 0) {
refcount_ptr[0] = refcount -% 1;
if (refcount == REFCOUNT_ONE_ISIZE) {
// If the first bit of the refcount is set, this variable is atomic.
if (refcount_ptr[0] & REFCOUNT_IS_ATOMIC_MASK != 0) {
const last = @atomicRmw(isize, &refcount_ptr[0], .Sub, 1, .monotonic);
if (last & REFCOUNT_VALUE_MASK == 1) {
free_ptr_to_refcount(refcount_ptr, alignment, elements_refcounted);
}
} else {
const last = @atomicRmw(isize, &refcount_ptr[0], .Sub, 1, .monotonic);
if (last == REFCOUNT_ONE_ATOMIC_ISIZE) {
refcount_ptr[0] = refcount -% 1;
if (refcount == 1) {
free_ptr_to_refcount(refcount_ptr, alignment, elements_refcounted);
}
}
Expand All @@ -431,19 +428,37 @@ pub fn isUnique(
std.debug.print("| is unique {*}\n", .{isizes - 1});
}

return rcUnique(refcount);
}

pub inline fn rcUnique(refcount: isize) bool {
switch (RC_TYPE) {
.normal => {
return refcount == REFCOUNT_ONE_ISIZE;
return refcount == 1;
},
.atomic => {
return refcount == REFCOUNT_ONE_ISIZE or refcount == REFCOUNT_ONE_ATOMIC_ISIZE;
return refcount & REFCOUNT_VALUE_MASK == 1;
},
.none => {
return false;
},
}
}

pub inline fn rcConstant(refcount: isize) bool {
switch (RC_TYPE) {
.normal => {
return refcount == REFCOUNT_MAX_ISIZE;
},
.atomic => {
return refcount & REFCOUNT_VALUE_MASK == REFCOUNT_MAX_ISIZE & REFCOUNT_VALUE_MASK;
},
.none => {
return true;
},
}
}

// We follow roughly the [fbvector](https://github.com/facebook/folly/blob/main/folly/docs/FBVector.md) when it comes to growing a RocList.
// Here is [their growth strategy](https://github.com/facebook/folly/blob/3e0525988fd444201b19b76b390a5927c15cb697/folly/FBVector.h#L1128) for push_back:
//
Expand Down Expand Up @@ -522,7 +537,7 @@ pub fn allocateWithRefcount(

const data_ptr = new_bytes + extra_bytes;
const refcount_ptr = @as([*]usize, @ptrCast(@as([*]align(ptr_width) u8, @alignCast(data_ptr)) - ptr_width));
refcount_ptr[0] = if (RC_TYPE == .none) REFCOUNT_MAX_ISIZE else REFCOUNT_ONE;
refcount_ptr[0] = if (RC_TYPE == .none) REFCOUNT_MAX_ISIZE else 1;

return data_ptr;
}
Expand Down Expand Up @@ -572,10 +587,10 @@ pub const UpdateMode = enum(u8) {
};

test "increfC, refcounted data" {
var mock_rc: isize = REFCOUNT_ONE_ISIZE + 17;
var mock_rc: isize = 17;
const ptr_to_refcount: *isize = &mock_rc;
increfRcPtrC(ptr_to_refcount, 2);
try std.testing.expectEqual(mock_rc, REFCOUNT_ONE_ISIZE + 19);
try std.testing.expectEqual(mock_rc, 19);
}

test "increfC, static data" {
Expand Down
77 changes: 3 additions & 74 deletions crates/compiler/gen_llvm/src/llvm/refcounting.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,12 +77,8 @@ impl<'ctx> PointerToRefcount<'ctx> {
pub fn is_1<'a, 'env>(&self, env: &Env<'a, 'ctx, 'env>) -> IntValue<'ctx> {
let current = self.get_refcount(env);
let one = match env.target.ptr_width() {
roc_target::PtrWidth::Bytes4 => {
env.context.i32_type().const_int(i32::MIN as u64, false)
}
roc_target::PtrWidth::Bytes8 => {
env.context.i64_type().const_int(i64::MIN as u64, false)
}
roc_target::PtrWidth::Bytes4 => env.context.i32_type().const_int(1_u64, false),
roc_target::PtrWidth::Bytes8 => env.context.i64_type().const_int(1_u64, false),
};

env.builder
Expand Down Expand Up @@ -131,74 +127,7 @@ impl<'ctx> PointerToRefcount<'ctx> {
.allocation_alignment_bytes(layout_interner)
.max(env.target.ptr_width() as u32);

let context = env.context;
let block = env.builder.get_insert_block().expect("to be in a function");
let di_location = env.builder.get_current_debug_location().unwrap();

let fn_name = &format!("decrement_refcounted_ptr_{alignment}");

let function = match env.module.get_function(fn_name) {
Some(function_value) => function_value,
None => {
// inc and dec return void
let fn_type = context.void_type().fn_type(
&[env.context.ptr_type(AddressSpace::default()).into()],
false,
);

let function_value = add_func(
env.context,
env.module,
fn_name,
FunctionSpec::known_fastcc(fn_type),
Linkage::Internal,
);

let subprogram = env.new_subprogram(fn_name);
function_value.set_subprogram(subprogram);

debug_info_init!(env, function_value);

Self::build_decrement_function_body(env, function_value, alignment, layout);

function_value
}
};

let refcount_ptr = self.value;

env.builder.position_at_end(block);
env.builder.set_current_debug_location(di_location);

let call = env
.builder
.new_build_call(function, &[refcount_ptr.into()], fn_name);

call.set_call_convention(FAST_CALL_CONV);
}

fn build_decrement_function_body<'a, 'env>(
env: &Env<'a, 'ctx, 'env>,
parent: FunctionValue<'ctx>,
alignment: u32,
layout: LayoutRepr<'a>,
) {
let builder = env.builder;
let ctx = env.context;

let entry = ctx.append_basic_block(parent, "entry");
builder.position_at_end(entry);

debug_info_init!(env, parent);

decref_pointer(
env,
parent.get_nth_param(0).unwrap().into_pointer_value(),
alignment,
layout,
);

builder.new_build_return(None);
decref_pointer(env, self.value, alignment, layout);
}

pub fn deallocate<'a, 'env>(
Expand Down
13 changes: 2 additions & 11 deletions crates/compiler/mono/src/code_gen_help/refcount.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ use bumpalo::collections::CollectIn;
use roc_error_macros::todo_lambda_erasure;
use roc_module::low_level::{LowLevel, LowLevel::*};
use roc_module::symbol::{IdentIds, Symbol};
use roc_target::PtrWidth;

use crate::code_gen_help::let_lowlevel;
use crate::ir::{
Expand Down Expand Up @@ -386,11 +385,7 @@ pub fn refcount_reset_proc_body<'a>(
};

// Constant for unique refcount
let refcount_1_encoded = match root.target.ptr_width() {
PtrWidth::Bytes4 => i32::MIN as i128,
PtrWidth::Bytes8 => i64::MIN as i128,
}
.to_ne_bytes();
let refcount_1_encoded = (1i128).to_ne_bytes();
let refcount_1_expr = Expr::Literal(Literal::Int(refcount_1_encoded));
let refcount_1_stmt = Stmt::Let(
refcount_1,
Expand Down Expand Up @@ -511,11 +506,7 @@ pub fn refcount_resetref_proc_body<'a>(
};

// Constant for unique refcount
let refcount_1_encoded = match root.target.ptr_width() {
PtrWidth::Bytes4 => i32::MIN as i128,
PtrWidth::Bytes8 => i64::MIN as i128,
}
.to_ne_bytes();
let refcount_1_encoded = (1i128).to_ne_bytes();
let refcount_1_expr = Expr::Literal(Literal::Int(refcount_1_encoded));
let refcount_1_stmt = Stmt::Let(
refcount_1,
Expand Down
6 changes: 2 additions & 4 deletions crates/compiler/test_gen/src/helpers/wasm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -315,13 +315,11 @@ where
rc_ptr += 4
}

// Dereference the RC pointer and decode its value from the negative number format
let rc_encoded = read_i32(&inst.memory, rc_ptr);
let rc = read_i32(&inst.memory, rc_ptr);

if rc_encoded == 0 {
if rc == 0 {
RefCount::Constant
} else {
let rc = rc_encoded - i32::MIN + 1;
RefCount::Live(rc as u32)
}
};
Expand Down
5 changes: 4 additions & 1 deletion crates/roc_std/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -583,7 +583,10 @@ impl Hash for U128 {
}

pub const ROC_REFCOUNT_CONSTANT: usize = 0;
pub const ROC_REFCOUNT_ONE: usize = isize::MIN as usize;
// The top bit indicates if refcounts should be atomic.
pub const ROC_REFCOUNT_IS_ATOMIC: usize = isize::MIN as usize;
// The remaining bits are the actual refcount.
pub const ROC_REFCOUNT_VALUE_MASK: usize = !ROC_REFCOUNT_IS_ATOMIC;

/// All Roc types that are refcounted must implement this trait.
///
Expand Down
4 changes: 2 additions & 2 deletions crates/roc_std/src/roc_str.rs
Original file line number Diff line number Diff line change
Expand Up @@ -925,7 +925,7 @@ impl BigString {
let ptr = self.ptr_to_refcount();
let rc = unsafe { std::ptr::read(ptr) as isize };

rc == isize::MIN
rc == 1
}

fn is_readonly(&self) -> bool {
Expand Down Expand Up @@ -973,7 +973,7 @@ impl BigString {
0 => {
// static lifetime, do nothing
}
isize::MIN => {
1 => {
// refcount becomes zero; free allocation
crate::roc_dealloc(self.ptr_to_allocation().cast(), 1);
}
Expand Down
4 changes: 2 additions & 2 deletions crates/roc_std/src/storage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ use core::num::NonZeroIsize;

/// # Safety
///
/// isize::MIN is definitely not zero. This can become
/// 1 is definitely not zero. This can become
/// https://doc.rust-lang.org/std/num/struct.NonZeroIsize.html#associatedconstant.MIN
/// once it has been stabilized.
const REFCOUNT_1: NonZeroIsize = unsafe { NonZeroIsize::new_unchecked(isize::MIN) };
const REFCOUNT_1: NonZeroIsize = unsafe { NonZeroIsize::new_unchecked(1) };

const _ASSERT_STORAGE_SIZE: () =
assert!(core::mem::size_of::<isize>() == core::mem::size_of::<Storage>());
Expand Down
Loading

0 comments on commit 10ea93e

Please sign in to comment.