-
Notifications
You must be signed in to change notification settings - Fork 86
MDBValue Optimizations #192
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
| /// </remarks> | ||
| #if NET5_0_OR_GREATER | ||
| [SkipLocalsInit] | ||
| #endif |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is MDBValue read-only?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This could be something to leverage, marking it readonly, and adding the in keyword in some methods such that the value is passed by ref automatically.
Here is the chatgpt explanation
Short answer:
They reduce copies, prevent accidental mutation, and enable better compiler optimizations—especially for larger structs.
Details:
1) readonly struct
Marking a struct as readonly guarantees it’s immutable after construction.
Advantages
- No defensive copies: The compiler knows instance methods won’t mutate fields, so it doesn’t create hidden copies when the struct is accessed through
in,readonlyfields, or properties. - Clear intent & safety: Prevents accidental field mutation and enforces immutability at compile time.
- Better optimizations: The JIT can make stronger assumptions, sometimes improving inlining and register usage.
- Thread-safety by design: Immutable value types are naturally safer to share.
Cost
- You must ensure all instance fields are readonly and methods don’t mutate state.
2) in parameters
in passes a struct by readonly reference instead of by value.
Advantages
- Avoids copying large structs: Especially useful when structs exceed ~16 bytes or are passed frequently.
- Expresses intent: Signals “this method will not modify the argument.”
- Interoperates with readonly structs: No defensive copies when calling methods on a
readonly struct.
Cost
- Indirection: Very small structs can be slower due to pointer dereferencing.
- Readonly rules: Attempting mutation causes compile errors or hidden copies if the struct isn’t readonly.
3) Using both together (best case)
This is where the real benefit shows up.
readonly struct+inparameters ⇒ zero copies, no defensive cloning, maximum safety.- Methods called on the struct don’t trigger hidden temporaries.
- Ideal for math types, vectors, coordinates, timestamps, and domain value objects.
Practical guidance
-
Use
readonly structwhen:- The type is logically immutable
- It’s used frequently or passed around a lot
-
Use
inwhen:- The struct is medium-to-large
- The method is hot-path or allocation/copy sensitive
-
Don’t bother for tiny structs (e.g., two ints).
Bottom line:
You get immutability guarantees, fewer copies, and better performance—when used selectively and intentionally.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I overlooked an aspect of the question.
- P/Invoke signatures must use ref - Functions like mdb_cursor_get write back values (native code sets size/data pointers). Can't use in.
- CompareFunction delegate must use ref - This is called FROM native code. The marshalling requires ref, not in:
delegate int CompareFunction(ref MDBValue left, ref MDBValue right); - IComparer passes by value - The standard interface signature is Compare(T x, T y), not Compare(in T x, in T y)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That said, there's probably several of the interop methods that could benefit from changing to 'in' instead of ref.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ref is fine, in just behaves like a ref without the need to use the ref keyword from the caller.
It's fine if not all methods use in or ref, like Compare. It's not a requirement, just that readonly allows the usage of in. We can still to ref when it's required.
mdb_cursor_getwrite back values...
Haven't checked the code, maybe a custom mutable and reusable struct (or class) could be used to pass to these, and then the library creates immutable ones.
Just brainstorming in case we can find patterns.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Even with readonly it can pass structs to be updated, as long as we know when it's done, ideally only during initialization:
private (MDBResultCode resultCode, MDBValue key, MDBValue value) Get(CursorOperation operation)
{
MDBValue mdbKey = default;
MDBValue mdbValue = default;
unsafe
{
var result = mdb_cursor_get(
_handle,
ref Unsafe.AsRef<MDBValue>(in mdbKey),
ref Unsafe.AsRef<MDBValue>(in mdbValue),
operation);
return (result, mdbKey, mdbValue);
}
}There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And this PR is fine, if you want to do changes maybe isolated them in separate PRs, no need to dead-lock PRs.
Optimizations suggested by @sebastienros in #191
I've only compared to v0.20.0, not the previous comparer implementations. The current branch outperforms the baseline v0.20.0 between 5% and 20%
Separately, it feels like there might be a more optimal route for sorted Guid as well (which I assume is a common key scenario). I'll do a little bit of digging, but if anyone knows something I don't already feel free to share.
Memory
Read Performance - All Comparers (1000 ops, 64B values)
Write Performance - All Comparers (1000 ops, 64B values)
Integer Keys (10000 ops, 4-byte keys)
Notes