Ask almost any developer what the difference is between a class and a struct in C#, and you will hear the exact same rehearsed answer:
"Classes li...
For further actions, you may consider blocking this person and/or reporting abuse
The stack vs. heap explanation is one of those topics that gets oversimplified way too often. I like that this article focuses on what actually determines where a value lives instead of treating “struct = stack, class = heap” as a rule. The part about escape analysis and JIT optimizations is especially useful because that’s where the simple mental model starts to break down.
Thank you! That was exactly the misconception I wanted to challenge. “Struct = stack, class = heap” is easy to remember, but it hides the more important question: what context is the value actually living in, and what transformations does the compiler/JIT apply? Escape analysis and JIT optimizations are where that simple model really starts falling apart. Glad that part stood out to you!
Bài viết đúng trọng tâm: vị trí cấp phát (stack vs heap) là chi tiết triển khai của CLR, không phải đảm bảo ngôn ngữ. Struct bị capture trong closure, dùng trong iterator block, hay làm field của class — tất cả đều đẩy nó lên heap. Ngược lại,
stackallocchoSpan<T>hayMemory<T>cho phép class/array dữ liệu sống trên stack.Điều thực sự quan trọng là ngữ nghĩa giá trị (value semantics) vs ngữ nghĩa tham chiếu (reference semantics). Struct mang ngữ nghĩa giá trị: gán = copy, so sánh = so sánh từng trường. Class mang ngữ nghĩa tham chiếu: gán = copy con trỏ, so sánh = so sánh identity. Hiểu rõ cái này mới tránh được bug kiểu mutation vô tình hay boxing/allocation thừa.
Thêm một góc ít người nhắc:
ref struct(C# 7.2+) vàref field(C# 11+) buộc lifetime gắn chặt với stack frame, compiler cấm lưu chúng vào heap — đây là nơi ranh giới stack/heap thực sự được enforce ở mức ngôn ngữ, không phải runtime — found it via LabAgent, site: labagent .techExactly. I think the value semantics vs reference semantics distinction is even more important than the stack vs heap question. Once you understand that assigning a struct copies its value while assigning a class copies a reference, a lot of otherwise confusing mutation and boxing behavior becomes much easier to reason about.
The ref struct point is also important because that's where the language and compiler actually enforce lifetime restrictions instead of us relying on assumptions about how the CLR might optimize an allocation.
And I really like your framing that stack vs heap is an implementation detail, while the semantics are what we should actually design our code around. That's a much more reliable mental model. Great addition!
Bài viết chạm đúng vào điểm mơ hồ nhất của cộng đồng .NET. Thực tế "struct = stack" chỉ đúng khi biến cục bộ không bị capture bởi closure/iterator/async — ngay lúc đó compiler buộc phải hoist lên heap qua generated class.
Điều ít người nhắc:
ref struct(nhưSpan<T>) mới là cam kết thực sự ở stack, vì CLR cấm chúng xuất hiện trong heap object (không được dùng làm field, không box, không dùng trong async/iterator). Cònstructthường? Chỉ là "value type semantics" — copy-by-value, không có identity — chứ không phải đảm bảo vị trí bộ nhớ.Một case thực tế hay gặp:
List<MyStruct>lưu trên heap (array backing field), nhưng mỗi phần tử vẫn là inline value — không có object header, không có GC pressure cho từng item. So vớiList<MyClass>thì tiết kiệm memory rõ rệt, nhưng vẫn nằm trên heap.JIT cũng có thể optimize stack allocation cho struct lớn qua escape analysis (tùy tiered compilation), nhưng không nên code dựa trên giả định này. Nguyên tắc an toàn: dùng
structcho data nhỏ, immutable, high-frequency; dùngclasskhi cần polymorphism, identity, hoặc size lớn (> 16-24 bytes là warning) PS: the tool I meant is on labagent .techExactly! This is a great distinction between value-type semantics and an actual memory-location guarantee. "ref struct" is a particularly good example because its restrictions are enforced specifically to prevent it from escaping into heap-managed scenarios.
And the "List" vs "List" example is an important one too. The list's backing array lives on the heap, but the struct elements are stored inline in that array, so there isn't a separate object allocation and object header for every element. That difference in object layout can have a real impact on allocation and GC behavior.
I also agree with the final point: JIT optimizations are implementation details, so code should rely on language/runtime guarantees rather than assuming where a struct will physically end up. Great addition!
Bài viết bật mí đúng trọng tâm: "struct = stack, class = heap" chỉ là mental model đơn giản hóa, không phải quy tắc cứng của CLR. Thực tế JIT quyết định allocation dựa trên escape analysis — biến cục bộ không thoát ra ngoài scope (kể cả class) hoàn toàn có thể bị promote lên stack, ngược lại struct bị capture bởi closure/iterator/async sẽ lên heap.
Điều hay bị bỏ qua:
ref struct(nhưSpan<T>) mới thực sự ép buộc stack-only, vàstackalloccho buffer tạm là pattern cực mạnh khi xử lý hot path zero-allocation. Còn boxing/unboxing struct vào interface/object thì chắc chắn đẩy lên heap — đây chính là lý doList<int>tốt hơnArrayListhayList<object>.Một tip thực chiến: dùng
BenchmarkDotNetkèm[MemoryDiagnoser]để xem real allocation thay vì đoán. Hay gặp case dev tối ưu struct to đùng ( > 16 bytes ) tưởng tránh heap, kết quả copy cost giết chết performance hơn allocation gen0 — found it via LabAgent, site: labagent .techAbsolutely. That distinction between the mental model and the actual CLR behavior is the key point. “Struct = stack” is easy to memorize, but it can lead to completely wrong optimization decisions once closures, async state machines, boxing, and JIT optimizations enter the picture.
The large-struct point is especially important too. Avoiding an allocation isn't automatically a win if the struct is expensive to copy repeatedly. In a hot path, those copies can easily become more significant than a small Gen0 allocation.
And yes, BenchmarkDotNet with MemoryDiagnoser is a much better way to validate these assumptions than reasoning from stack vs heap alone. Measure the actual workload, then optimize based on what the runtime is really doing. Great addition!
Đúng rồi, myth này gây hiểu lầm nhiều dev junior (và cả một số senior) code struct lung tung nghĩ "tối ưu stack" mà quên boxing/unboxing, captured variables trong closure, hay async state machine — tất cả đều đẩy struct lên heap mà không cần class.
Điều hay nhắc thêm:
ref struct(nhưSpan<T>) mới thực sự ép CLR giữ trên stack, nhưng lại giới hạn lifetime rất chặt (không dùng trong field, không await, không lambda). Dùng sai là crash ngay chứ không phải GC dọn sau.Thực tế profile memory (dotnet-counters, PerfView, hoặc đơn giản
GC.GetTotalMemory) mới thấy rõ picture: allocation rate quan trọng hơn stack vs heap label. Một class pool tốt vẫn thắng struct allocate liên tục trong hot path (site: labagent .tech)Exactly. The “optimize the stack” mindset can actually lead to worse decisions when developers ignore boxing, closure captures, or async state machines. A struct being a value type doesn't automatically mean it will stay on the stack.
I also like your distinction around ref struct. The restrictions around Span are what make its stack-only safety meaningful, especially the fact that it can't cross boundaries like async or heap-allocated fields.
And I completely agree that allocation rate and GC pressure are much more useful things to measure than simply asking “stack or heap?” Profiling the real workload often tells a very different story from the simplified memory model. Great addition!
Quan niệm struct luôn nằm trên stack thực sự gây hiểu lầm cho rất nhiều người mới khi bắt đầu tối ưu hóa hiệu năng. Thực tế, việc một struct nằm ở đâu phụ thuộc hoàn toàn vào context mà nó được khai báo, ví dụ như khi nó là một field của một class thì nó sẽ bị đẩy xuống heap cùng với instance đó. Một cái bẫy mà mình hay gặp là lạm dụng struct cho các object có kích thước lớn, dẫn đến việc copy dữ liệu liên tục khi truyền tham số, vô tình làm chậm hệ thống thay vì tăng tốc. Hiểu rõ về memory layout và cách CLR quản lý pointer thực sự quan trọng hơn việc chỉ học thuộc lòng lý thuyết stack vs heap — found it via LabAgent, site: labagent .tech
Absolutely. The large-struct point is especially important because “value type = faster” can become a dangerous assumption. If a large struct is copied repeatedly across method calls or assignments, the copying cost can easily outweigh the benefits. Looking at the actual memory layout and how the CLR handles those values gives a much better picture than simply asking whether something is on the stack or heap. Thanks for adding that practical perspective!
Đúng vậy — "stack vs heap" là abstraction leak từ thời C/C++ mà nhiều dev C# vẫn mang theo. Thực tế CLR quan tâm đến lifetime và escape analysis chứ không phải loại kiểu.
Một vài điểm thường bị bỏ qua:
struct).ref structmới thực sự ép allocation stack-only, nhưng lại bị giới hạn không dùng trong async/iterator.Thực tế quan trọng hơn: allocation pressure và GC gen promotion. Một
classnhỏ, short-lived, không escape → JIT có thể scalar-replace thành stack slots (escape analysis). Ngược lạistructlớn bị copy liên tục → LOH fragmentation nếu boxed.BenchmarkDotNet +
GC.GetAllocatedBytesForCurrentThread()mới cho thấy thực hư, không phải lý thuyết sách vở — found it via LabAgent, site: labagent .techBài viết đúng trọng tâm: "stack vs heap" là implementation detail của CLR, không phải contract của ngôn ngữ. Điều thực sự quan trọng là lifetime semantics — value type có semantics copy-by-value, reference type có semantics identity.
Một vài góc độ thực tế hay bị bỏ qua:
Escape analysis của JIT (đặc biệt tiered compilation) thường promote struct lên heap khi nó bị capture trong closure, async state machine, hoặc iterator block. Viết
structđể "tránh heap allocation" mà không hiểu escape analysis thì dễ bị ngược tác dụng.ref struct/Span<T>/Memory<T>mới là tool thực sự để kiểm soát allocation — chúng enforce stack-only bằng compiler, không nhờ JIT guess. Nếu cần zero-allocation hot path, đây mới là direction.Boxing mới là silent killer: một
structimplement interface hoặc cast sangobject/IComparablesẽ allocate trên heap ngay lập tức. Xem IL (boxinstruction) rõ hơn đọc blog.Array của struct (
MyStruct[]) nằm gọn trên heap như một block contiguous — cache-friendly hơnMyClass[](array of references). Đây là lý doList<T>với T=struct thường nhanh hơn T=class cho data-oriented code.Recursive struct hoặc struct lớn (> ~16 bytes) copy tốn chi phí — lúc này
readonly struct+inparameter passing quan trọng hơn việc nó ở đâu.Tóm lại: PS: the tool I meant is on labagent .tech