Skip to content
Search lessons, topics, tests…
Esc

    ↑ ↓ moveEnter openEsc close

    Module 1 · Values, Parameters and Byrefs · Lesson 2 of 34

    Parameters are passed by value, and the cost of large structs

    2.1 Every parameter is passed by value unless declared otherwise

    For a value-type argument, by-value passing gives the callee a copy of the value. For a reference-type argument, by-value passing gives the callee a copy of the object reference. The referenced object is not duplicated.

    C#
    static void ChangeNumber(int value) => value = 99;
    
    static void ChangePerson(Person person)
    {
        person.Name = "Ada";
        // mutates the shared object
        person = new Person("Grace"); // replaces only the local reference copy
    }
    Text
    VALUE TYPE PARAMETER                       REFERENCE TYPE PARAMETER
    caller                                     caller
    +-------------------+                      +-------------------+
    | n: 10             |                      | p: object ref ----+----> Person object
    +---------+---------+                      +---------+---------+
              | copy 10                                  | copy object ref
              v                                          v
    callee                                     callee
    +-------------------+                      +-------------------+
    | value: 10         |                      | person: ref ------+----> same Person
    +-------------------+                      +-------------------+
    assigning value does not change n          person.Name mutates shared object

    2.2 Large structs make the copy visible in the cost model

    Consider a fixed-size 64-byte struct. Semantically, a by-value call creates an independent parameter value. The physical copy may happen in registers, stack memory, a caller-provided area, or be optimized away, but the JIT must preserve independent-value behavior.

    C#
    public readonly struct Block64
    {
        public readonly long A, B, C, D, E, F, G, H;
    
        public Block64(long seed) =>
            (A, B, C, D, E, F, G, H) =
            (seed, seed + 1, seed + 2, seed + 3,
             seed + 4, seed + 5, seed + 6, seed + 7);
    }
    
    static long SumByValue(Block64 block) =>
        block.A + block.B + block.C + block.D +
        block.E + block.F + block.G + block.H;

    Cost model

    For a fixed C# type, passing is still constant-time in Big O notation. A more useful systems model is O(S) bytes copied, where S is the struct’s payload size. The algorithm around the call may remain O(1), while its constant factor grows with copied bytes and call frequency.

    Plain parameter passing normally allocates 0 managed heap bytes. Copy cost and allocation cost are separate questions.

    Under The Hood

    CLR / IL. A by-value parameter’s signature contains Block64, not Block64&. IL reads the parameter with ldarg. Struct copies can appear as ldobj, stobj, or cpobj, but compilers and JITs are free to use other equivalent forms.

    JIT. The JIT can inline the method, promote a struct into individual fields, pass pieces in registers according to the platform ABI, or remove a copy whose independence is unobservable. Large or address-exposed structs are less likely to remain fully enregistered.

    Memory / GC. A struct containing managed-reference fields carries GC layout metadata so copied references remain visible to the GC. The struct copy is not automatically a heap allocation.[S10]

    Performance. By-value can outperform byref for small structs because it avoids an indirection and gives the JIT stronger non-aliasing freedom. Never apply in mechanically to every value type.

    Naive optimization

    C#
    static int Abs(in int value) => Math.Abs(value);

    Adding in to a 4-byte integer creates a byref contract without avoiding meaningful copy work. It can make calling and optimization less convenient.

    Production default

    Pass small primitive and small immutable value types by value. Consider readonly byref parameters for demonstrably large structs in hot paths, then verify with benchmarks on the target runtime and architecture.

    Sign in to mark lessons done and keep your place in the course.Sign in