#27508

error() heap-allocates a MessageError on every call; provide a zero-alloc sentinel error like `none` for hot not-found paths

Openenghitaloopened 4mo ago

Describe the feature

error('msg') heap-allocates a MessageError on every call and boxes it into the IError interface. This happens on the hot path of any !T ("result") function that returns an error, even when the caller discards the result with or {} — the allocation happens inside the callee, before it returns, regardless of what the caller does with the value.

Current implementation, vlib/builtin/chan_option_result.v:128-134:

@[inline]
pub fn error(message string) IError {
	trace_error(message)
	return &MessageError{
		msg: message
	}
}

The & heap-allocates a MessageError (GC-managed). The compiler lowers error(...) to builtin___v_error(...). Generated C (v -printfn builtin___v_error file.v):

inline IError builtin___v_error(string message) {
	;
	GC_reachable_here(&message);
	return I_MessageError_to_Interface_IError((HEAP(MessageError, ((MessageError){.msg = message,.code = 0,}))));
}

HEAP(type, expr) expands to ((type*)builtin__memdup((void*)&((type[]){expr}[0]), sizeof(type))) — a real allocation (builtin__memdup) plus interface boxing, per call.

This contrasts with none, which already has a zero-allocation cached sentinel (vlib/builtin/chan_option_result.v:203-211):

const none__ = IError(&None__{})

struct None__ {
	Error
}

Use Case

A very common pattern is a "lookup / not-found" function on a hot path:

fn find(x int) !int {
	if x < 0 {
		return error('not found')
	}
	return x
}

fn main() {
	mut n := 0
	for i := -1_000_000; i < 0; i++ {
		find(i) or { n++ } // result discarded, yet each error() above still allocated
	}
	println(n)
}

Every miss allocates a MessageError on the heap, even though the error object is immediately discarded. For tight loops, caches, parsers, and any "fast not-found path", this is a per-call allocation that cannot currently be avoided while still using the idiomatic !T / error() mechanism.

Generated main__find (identical under default and -prod):

VV_LOC _result_int main__find(int x) {
	if (x < 0) {
		return (_result_int){ .is_error=true, .err=builtin___v_error(_S("not found")), .data={E_STRUCT} };
	}
	...
}

The callee always builds the boxed error (builtin___v_error → HEAP(...) → builtin__memdup) before returning, so or {} at the call site cannot avoid it.

V already proves a zero-alloc error return is possible: ?T functions that return none compile to a cached const, no allocation:

return (_option_int){ .state=2, .err=_const_none__, .data={E_STRUCT} };

There is no equivalent for the very common "return an error with no extra state" case.

Proposed Solution

Provide a non-allocating, cached/sentinel error path for the common stateless-error case, mirroring how none works. Possible approaches (any one would address this):

  1. A cached/static sentinel error for the stateless case (e.g. a public error_none constant, or reusing a single static IError), so a "not found" fast path can be expressed allocation-free.
  2. A non-allocating error() for compile-time-constant messages. String literals are already static, so the MessageError could be emitted as a static instance instead of HEAP(...) when the message is a constant.
  3. A compiler optimization: when the result of error(...) is provably discarded by the caller's or {} (message unused), skip the HEAP allocation and return a cached sentinel.

The none machinery in vlib/builtin/chan_option_result.v:203-211 is the reference implementation pattern.

Exact code involved: vlib/builtin/chan_option_result.v:129-134 (error), its lowering to builtin___v_error, and the matching HEAP(MessageError, ...) expansion above.

Other Information

Reproduction verified with v run, v -printfn ..., and v -prod on the version below. Swapping !int → ?int and return error('not found') → return none removes the allocation (.err=_const_none__), demonstrating both the asymmetry and that a zero-alloc path is feasible.

No existing duplicate was found via gh search issues --repo vlang/v for terms like "error allocation", "MessageError", "zero allocation error", "cached error", "IError boxing".

Acknowledgements

  • I may be able to implement this feature request
  • This feature might incur a breaking change

Version used

V 0.5.1 6dd9033de29819f9ada4011f783b1d8b9d47d7e8.eb1d47b

Environment details (OS name and version, etc.)

linux, Ubuntu 24.04 LTS; cc (GCC) 14.2.0

NOTE

You can use the 👍 reaction to increase the issue's priority for developers.

Please note that only the 👍 reaction to the issue itself counts as a vote. Other reactions and those to comments will not be taken into account.

Activity

0

No activity yet.