Skip to content

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

Description

@enghitalo

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_errorHEAP(...)
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 6dd9033.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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Feature/Enhancement RequestThis issue is made to request a feature or an enhancement to an existing one.Result TypeBugs/feature requests, that are related to `!Type`.Unit: vlibBugs/feature requests, that are related to the vlib.

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions