Misconceptions for novice C # developers. Trying to answer standard questions

I recently had the opportunity to chat with a fairly large number of novice C # developers. Many of them are interested in language and platform, and this is very cool. Among green juniors obscurantism is common about obvious (just read a book about memory) things. And this also prompted me to create this article. The article is primarily aimed at beginning developers, but I think that many facts will be useful to practicing engineers. Well, the most obvious and uninteresting errors, of course, are omitted. Here are the most interesting and significant, especially from the point of view of passing the interview.











#one. Mantra about 3 generations in any situation



This is more an inaccuracy than an error. The question about the "garbage collector in C #" for the developer has become a classic and few people will start to answer smartly about the concept of generations. However, for some reason, few people pay attention to the fact that the great and terrible garbage collector is part of runtime. Accordingly, I would have made it clear that it wasn’t a finger, and would have asked what kind of runtime environment was involved. The query “garbage collector in c #” on the Internet can find more than a lot of similar information. However, few people mention that this information refers to the CLR / CoreCLR (as a rule). But do not forget about Mono, a lightweight, flexible and embeddable runtime that has occupied its niche in mobile development (Unity, Xamarin) and is used in Blazor. And for the respective developers, I would advise you to inquire about the details of the assembly device in Mono. For example, at the request “mono garbage collector generations”, you can see that there are only two generations - nursery and old generation (in the new and fashionable garbage collector - SGen ).



# 2 Mantra about 2 stages of garbage collection in any situation



Not so long ago, the sources of the garbage collector were hidden from everyone. However, there has always been interest in the internal structure of the platform. Therefore, information was extracted in different ways. And some inaccuracies in the reverse engineering of the collector led to the myth that the collector works in 2 stages: marking and cleaning. Or even worse, 3 stages - marking, cleaning, compression.



However, everything changed when the people of fire unleashed a war with the advent of CoreCLR and the source code for the collector. The compiler code for CoreCLR was taken entirely from the CLR version. Nobody wrote it from scratch, respectively, almost everything that can be learned from the CoreCLR source code will be true for the CLR as well. Now, to understand how something works, just go to github and find it in the source code or read readme . There you can see that there are 5 phases: marking, planning, updating links, compacting (deletion with relocation) and deletion without relocations (this is difficult to translate). But formally it can be divided into 3 stages - marking, planning, cleaning.



At the stage of marking, it turns out which objects should not be collected by the collector.

At the planning stage, various indicators of the current state of memory are calculated and the data necessary at the cleaning stage are collected. Thanks to the information received at this stage, a decision is made on the need for compacting (defragmentation), it also calculates how much you need to move objects, etc.



And at the stage of cleaning , depending on the need for compacting, links can be updated and compacting or deleting without moving.



# 3 Allocating memory on the heap is as fast as on the stack



Again, inaccuracy rather than absolute untruth. In the general case, of course, the difference in the speed of memory allocation is minimal. Indeed, in the best case, with bump pointer allocation , memory allocation is just a pointer shift, as on the stack. However, factors such as assigning a new object to the old field (which will affect the write barrier , updating the card table — a mechanism that allows you to track links from the older generation to the younger), the presence of a finalizer (you must add the type to the appropriate queue) can affect the allocation of memory on the heap. and others. It is also possible that the object will be recorded in one of the free holes in the heap (after assembly without defragmentation). And finding such a hole, though fast, is obviously slower than a simple pointer shift. Well, of course, each created object brings the next garbage collection closer. And in the next procedure for allocating memory, it can happen. Which, naturally, will take some time.



#four. Definition of reference, meaningful types and packaging through the concepts of stack and heap



Right classic, which, fortunately, is not so common.



The reference type is located on the heap. Significant on the stack. Surely many have heard these definitions very often. But not only is this only a partial truth, so defining concepts through leaked abstraction is not a good idea. For all definitions, I suggest that you refer to the CLI standard - ECMA 335 . First, it’s worth clarifying that types describe values. So, the reference type is defined as follows - the value described by the reference type (link) indicates the location of another value. For a significant type, the value described by it is autonomous (self-contained). About where these or those types of words are located. This is a leaked abstraction that you should still know.



A significant type may be located:



  1. In dynamic memory (heap), if it is part of an object located on the heap, or in the case of packaging;
  2. On the stack, if it is a local variable / argument / return value of a method;
  3. In registers, if it allows the size of a significant type and other conditions.


The reference type, namely, the value to which the link points, is currently located on the heap.



The link itself can be located in the same place as the significant type.



Packaging is also not determined through storage locations. Consider a brief example.



C # Code
public struct MyStruct { public int justField; } public class MyClass { public MyStruct justStruct; } public static void Main() { MyClass instance = new MyClass(); object boxed = instance.justStruct; }
      
      







And the corresponding IL code for the Main method



IL code
  1: nop 2: newobj instance void C/MyClass::.ctor() 3: stloc.0 4: ldloc.0 5: ldfld valuetype C/MyStruct C/MyClass::justStruct 6: box C/MyStruct 7: stloc.1 8: ret
      
      







Since the significant type is part of the reference, it is obvious that it will be located on the heap. And the sixth line makes it clear that we are dealing with packaging. Accordingly, the typical definition of “copy from stack to heap” fails.



To determine what a package is, for starters it is worth saying that for each significant type CTS (common type system) defines a reference type, which is called a packed type. So, packaging is an operation on a significant type that creates the value of the corresponding packed type containing a bitwise copy of the original value.



#four. Events - a separate mechanism



Events exist from the first version of the language and questions about them are much more common than the events themselves. However, it is worth understanding and knowing what it is, because this mechanism allows you to write very loosely coupled code, which is sometimes useful.



Unfortunately, often an event is understood as a separate instrument, type, mechanism. This is particularly promoted by the type from BCL EventHandler , whose name suggests that it is something separate.



Defining an event should begin by defining the properties. I have long drawn such an analogy for myself, and recently saw that it was drawn in the CLI specification.



The property defines the named value and the methods that access it. That sounds pretty obvious. We pass to events. CTS supports events as well as properties, BUT methods for access are different and include methods for subscribing and unsubscribing from an event. From the C # language specification, the class defines an event ... which is reminiscent of a field declaration with the addition of the event keyword. The type of this declaration must be the type of delegate. Thanks to the CLI standard for the definitions.



So, this means that the event is nothing more than a delegate that exposes only part of the functionality of delegates - adding another delegate to the list for execution, removing it from this list. Inside the class, the event is no different from a simple field like a delegate.



#five. Managed and unmanaged resources. Finalizers and IDisposable



There is absolute confusion when dealing with these resources. This is largely facilitated by the Internet with a thousand articles on the correct implementation of the Dispose pattern. Actually, there is nothing criminal in this pattern - a modified template method for a specific case. But the question is whether it is needed at all. For some reason, some people have an irresistible desire to implement a finalizer for every sneeze. Most likely, the reason for this is not a full understanding of what is an “unmanaged resource”. And the lines about the fact that in the finalizers, as a rule, unmanaged resources are released due to this incomplete understanding, pass by and do not remain in the head.



An unmanaged resource is a resource that is not managed (however strange it may be). A managed resource , in turn, is one that is allocated and released by the CLI automatically through a process called garbage collection. I brazenly stripped this definition from the CLI standard. But if you try to explain more simply, unmanaged resources are those that the garbage collector does not know about. (Strictly speaking, we can give the collector some information about such resources using GC.AddMemoryPressure and GC.RemoveMemoryPressure, this can affect the collector’s internal tuning). Accordingly, he will not be able to take care of their release himself, and therefore we must do it for him. And there can be many approaches to this. And so that the code does not dazzle with the diversity of the imagination of the developers, 2 generally accepted approaches are used.



  1. The IDisposable interface (and its asynchronous version of IAsyncDisposable). It is monitored by all code analyzers, so it's hard to forget about its call. Provides a single method - Dispose. And compiler support is the using statement. An excellent candidate for the body of the Dispose method is to call a similar method of one of the fields in the class or to release an unmanaged resource. Called explicitly by the class user. The presence of this interface in the class implies that upon completion of work with the instance, you need to call this method.

  2. Finalizer. At its core is insurance. Invoked implicitly, at an undefined time, during garbage collection. Slows down memory allocation, the work of the garbage collector, extends the lifetime of objects at least until the next assembly, or even longer, but it is called by itself, even if no one called it. Because of its non-deterministic nature, only unmanaged resources should be freed in it. You can also find examples in which the finalizer was used to resurrect the object and organize the pool of objects in this way. However, such an implementation of a pool of objects is definitely a bad idea. Like trying to log in, throw exceptions, access the database and thousands of similar actions.



And you can easily imagine the situation when writing a library critical to performance, which internally uses unmanaged resources, that it manages just the competent handling of this resource, freeing up memory carefully manually. When writing such high-performance libraries, OOP, support, and others like it, goes by the wayside.



And contrary to the assertion that Dispose violates the concept in which the CLR will do everything for us, force us to do something ourselves, remember something, etc., I will say the following. When working with unmanaged resources, you must be prepared that they are not managed by anyone other than you. And in general, situations in which these resources will be used in the enterprices are almost never encountered. And in most cases, you can get by with wonderful wrapper classes like SafeHandle, which provides critical resource finalization, preventing them from being prematurely built.



If, for one reason or another, there are a lot of resources in your application that require additional actions for release, then you should take a look at JetBrains' excellent pattern, Lifetime. But you should not use it when you see the first IDisposable object.



# 6 Stream stack, call stack, computing stack and
  Stack <T> 



The last paragraph added laughter for the sake of it; I do not think that there are those who attribute the latter to the previous two. However, there is a lot of confusion about what a stream stack, call stack, and computational stack are.



The call stack is a data structure, namely a stack, for storing return addresses, for returning from functions. The call stack is a more logical concept. It does not regulate where and how information should be stored for return. It turns out that the call stack is the most common and native stack to us [i.e.
  Stack <T> 
: trollface:]. Local variables are stored in it, parameters are passed through it, and return addresses are stored in it when the CALL instruction and interrupts are called, which are subsequently used by the RET instruction to return from the function / interrupt. Go ahead. One of the main jokes of the stream is a pointer to the instruction, which is executed further. A thread in turn executes instructions that combine into functions. Accordingly, each thread has a call stack. Thus, it turns out that the stream stack is the call stack. That is, the call stack of this stream. In general, it is also referred to under other names: software stack, machine stack.



It was considered in detail in the previous article .

Also, the definition of the call stack is used to indicate the chain of calls of specific methods in any language.



Computation stack (evaluation stack) . As you know, C # code is compiled into IL code, which is part of the resulting DLL (in the most general case). And just at the heart of the runtime that absorbs our DLLs and executes IL code is the stack machine. Almost all IL instructions operate with a certain stack. For example, ldloc loads a local variable under a specific index onto the stack. Here, the stack refers to a certain virtual stack, because in the end, this variable can with high probability be in registers. Arithmetic, logical, and other IL instructions operate on variables from the stack and put the result there. That is, calculations are made through this stack. Thus, it turns out that the computing stack is an abstraction in runtime. By the way, many virtual machines are stack-based.



# 7 More threads - faster code



It intuitively seems that processing data in parallel will be faster than alternately. Therefore, armed with knowledge about working with threads, many try to parallelize any cycle and computation. Almost everyone already knows about the overhead, which contributes to the creation of the thread, so they use the threads from ThreadPool and Task famously. But the overhead of creating a stream is far from the end. Here we are dealing with another leaked abstraction, the mechanism that the processor uses to improve performance - the cache. And as often happens, the cache is a double-edged blade. On the one hand, it significantly speeds up the work with sequential access to data from one stream. But on the other hand, when several threads work, even without the need to synchronize them, the cache not only does not help, but it also slows down. Extra time is spent on cache invalidation, i.e. maintaining relevant data. And do not underestimate this problem, which at first seems like a trifle. A cache-efficient algorithm will execute one thread faster than a multi-threaded one, which uses the cache inefficiently.



Also trying to work with a drive from many threads is suicide. The disk is already an inhibitory factor in many programs that work with it. If you try to work with it from many threads, then you need to forget about speed.



For all definitions, I recommend contacting here:



C # Language Specification - ECMA-334

Just good sources:

Konrad Kokosa - Pro .NET Memory Management

CLI specification - ECMA-335

CoreCLR developers about runtime - Book Of The Runtime

From Stanislav Sidristy about finalization and more - .NET Platform Architecture



All Articles