
Every day, cross-platform development for .NET is becoming more real. And after the recent
announcement of official support for Linux / MacOS, the happy future has become a little closer. The above picture has lost its former relevance, because the source will now be under MIT. However, it is possible to write cross-platform .NET-applications for a long time - Mono helps us in this. But the attitude towards him in the community is rather ambiguous. I often have to hear sayings like “Mono tupit, everything works three times slower for him” or “Under Mono, nothing runs normally.” Moreover, it is very rare to hear specific facts from these people. The questions “What specifically tupit?” Or “What specifically does not work?” Plunge them into a stupor. Not all (some are capable of constructive discussion), but most. Most often start angered answers in the spirit of “Yes, nothing works at all! And if it works, then very slowly! ” At the end of the conversation, it seems that each final machine command under Mono runs several times slower, and in half of the sources there are
throw new Exception()
.
In this post I would like to share my experience a little bit. Not so long ago, we ported our product
PassportVision (
announcement on Habré ) under Linux. I can say that it works quite normally. Yes, a bit slower than under Windows on the classic .NET from Microsoft (hereinafter referred to as MS.NET). But it works quite stably, but the drop in performance is not fundamental. At the same time, our product is quite large and quite falls under the category of enterprise, and we use the capabilities of C # /. NET to the fullest. So it’s really possible to start a large server application for .NET - there would be a desire. I also had the opportunity to talk with different developers who write something for Mono - most of the stories are successful.
')
But why, then, is there so much negativity towards Mono? I consider that a problem that people not especially want to understand a difference between rantayma. We once launched a .NET application for Linux on Mono 2.4, but it did not start right away - everything, Mono is completely bad, we will not use it. But in the end, the fault is a single method, whose implementation is slightly different from MS.NET. New versions of Mono come out every couple of months, the implementation has long been fixed, but people still continue to walk and find fault with poor Mono, not wanting to understand the details.
Today I will give a few examples of how different runtime may differ.
Of course, it’s impossible to bring all the differences - you can write a whole book about it. Yes, and supercool problems from the production-code, too, will not work, it is often too difficult to separate them from the context of the software architecture and bring in the form of a small clear piece of code. My current task is to simply convey the idea of ​​what may differ in Mono and MS.NET. I hope this helps beginner Mono programmers to take a broader look at the problems that arise and begin to deal with them, instead of turning around and leaving with the words "Mono is stupid."
Example 1. Different versions of the compiler
Let's start with a very simple example. Suppose we have some C # program, and we want to compile it. As we know, the standard compiler simply translates our C # code into the corresponding IL code and arranges it as an assembly. Moreover, the translation process does not include any particularly clever optimization, because JIT is responsible for them. The translation is quite simple, and an experienced .NET developer can often think about which IL teams will be in the end. But for some reason, many developers are confident that this process is
unambiguous . I had some fairly heated discussions in which they tried to prove to me that “there is exactly one way to display the source program in IL”. Usually, people motivate this with the remarkable argument
“There are specifications!” . I want to assure you that a lot of useful information has been written in the specifications, but nothing is said about the only option for broadcasting an arbitrary program. Moreover, in different versions of the compiler, the translation logic may differ. I picked up a very simple example that illustrates this. Consider the following code:
var numbers = new int[] { 1, 2, 3 };
In Mono 2.10+ and MS.NET, we will see something like the following:
IL_0000: ldc.i4.3 IL_0001: newarr [mscorlib]System.Int32 IL_0006: dup IL_0007: ldtoken field valuetype '<PrivateImplementationDetails>{de495e46-bf42-4605-a020-39ddddfe413c}'/'$ArrayType=12' '<PrivateImplementationDetails>{de495e46-bf42-4605-a020-39ddddfe413c}'::'$field-0' IL_000c: call void class [mscorlib]System.Runtime.CompilerServices.RuntimeHelpers::InitializeArray(class [mscorlib]System.Array, valuetype [mscorlib]System.RuntimeFieldHandle) IL_0011: stloc.0 // ... .field assembly static valuetype '<PrivateImplementationDetails>{de495e46-bf42-4605-a020-39ddddfe413c}'/'$ArrayType=12' '$field-0' at D_00004000 // ... .data D_00004000 = bytearray ( 01 00 00 00 02 00 00 00 03 00 00 00) // size: 12
As you can see from the example, the values ​​of the source elements are stored in a special byte array. But at the time of Mono 2.4.4 (you may not believe it, but to this day there are people who use it) the translator was not so smart:
IL_0000: ldc.i4.3 IL_0001: newarr [mscorlib]System.Int32 IL_0006: dup IL_0007: ldc.i4.0 IL_0008: ldc.i4.1 IL_0009: stelem.i4 IL_000a: dup IL_000b: ldc.i4.1 IL_000c: ldc.i4.2 IL_000d: stelem.i4 IL_000e: dup IL_000f: ldc.i4.2 IL_0010: ldc.i4.3 IL_0011: stelem.i4 IL_0012: stloc.0
In fact, the following happens here:
var numbers = new int[3]; numbers[0] = 1; numbers[1] = 2; numbers[2] = 3;
Functionally, nothing has changed, but now we have the realization that different versions of the compiler can produce different code. But this example is not so interesting, since the behavior of the code remains the same (except for a small performance drop for older versions of Mono). Let's move on to more interesting examples.
Example 2. Differences in working with IL
Practice shows that a simple "trivial" code works about the same as under MS.NET, and under Mono. And the problems most often start when not so trivial things appear in the code. Not so long ago, John Skit wrote a wonderful post
“When is a string not a string?” (
Russian translation from
impwx ). In short, the content of the post is reduced to the consideration of the following example:
[Description(Value)] class Test { const string Value = "X\ud800Y"; }
A string in C # is a sequence of words in UTF-16. And the value of
"X\ud800Y"
not particularly good, because includes the
0xD800
word of the surrogate pair
0xD800
, after which the lower word would have to go (interval
0xDC00..0xDFFF
), but instead of it comes
Y
(
0x0059
). The problems begin because IL code uses UTF-8 to store the arguments of the attribute constructor. However, at John Skit everything is very well written, I advise everyone to read the original post.
I was interested in how MS.NET and Mono will behave in this difficult situation (
detailed note ). And they will behave in different ways. The first difference can be seen at compile time. MS.NET will put the value of the string in the metadata in the form of
58 ED A0 80 59
, and Mono in the form of
58 59 BF BD 00
(both values ​​are non-valid UTF-8 lines). The second difference can be observed by running the received applications. MS.NET will be able to run both versions and successfully retrieve the value of the attribute argument (in the form
0058 fffd fffd 0059
and
0058 0059 fffd fffd 0000
respectively), and Mono will choke with such an invalid line and return
null
in each of the cases. Because of this, a small example of John Skit fell immediately when I tried to run it under Mono.
The problems lie in different implementations of converting strings between encodings. I like this example because, with its minimalist character, it shows that MS.NET and Mono can both form different IL code when compiled, and work differently with it when the application starts.
Example 3. Mono bugs
Yes, there are
bugs in Mono. And yes, they are not very few, you have to live with it. Fortunately, it is not often necessary to fall for them, and from open bugs there are not so many critical ones. I'll tell you one story: when porting PassportVision to Mono, I came across one not very pleasant moment. I had to deal with code generation and create a part of logic on the fly depending on a number of conditions. I created new types through
TypeBuilder , and these types implemented certain interfaces. I learned
a lot about
new code generation in Mono, but most of them are very difficult to briefly tell apart from the context. But one bug was reproduced very easily: the
TypeBuilder.CreateType()
method did not check the scope of the declared interfaces. I.e. code
private interface IFoo {} // ... void Main() { var assemblyBuilder = Thread.GetDomain().DefineDynamicAssembly( new AssemblyName("FooAssembly"), AssemblyBuilderAccess.Run); var moduleBuilder = assemblyBuilder.DefineDynamicModule("FooAssembly"); TypeBuilder typeBuilder = moduleBuilder.DefineType("Foo", TypeAttributes.Public, typeof(object), new[] { typeof(IFoo) }); typeBuilder.CreateType(); }
fell under MS.NET (as it should be), but it worked fine under Mono. This situation did not suit me very much, so I went and started a
bug . We must pay tribute to the guys from Xamarin - they
corrected him after 5 days. If you use Mono 3.8.0 (which was released in August), then you still have this bug, but Mono 3.10.0
came out with a fix .
This problem is not particularly critical, but it still gave me a number of inconveniences. But in the bug tracker still hangs
about 5,000 open problems. Therefore, you need to keep in mind that certain errors in Mono can be and that from version to version the behavior of some minor moments may change (perhaps because of my bug report someone has stopped working after the update). If possible, it is better to sit under the latest stable version of Mono.
Example 4. .NET bugs
After such conversations, they usually begin to fly stones in the direction of Mono: they say that it is bad, there are bugs in it. And somehow they forget that Microsoft bugs do too. The following story cost a certain number of nerve cells to one friend of mine. After the next commit, the build server reported that the tests had dropped. And on the working machine, they passed on excellent, but on the build server they fell. I’ll leave out a fascinating description of how the search for bugs occurred, and I’ll get to the point: there were different versions of the .NET Framework on the build server and the host machine: 4.0 and 4.5. And the bug itself was that the line
new Uri("http://localhost/%2F1").AbsoluteUri
produces a different result depending on TargetFramework (
detailed note ). In 4.0, there was a
bug with slash escaping (aka
%2F
): this line returned
http://localhost//1
. In 4.5, the bug was corrected in accordance with
RFC 3986 , the new result is
http://localhost/%2F1
.
And what about Mono? And there was a similar
bug that was
corrected in August (the fix is ​​included in Mono 3.10.0). This example teaches us that the same problems may arise in different implementations of .NET and be corrected with new versions. A truly cross-platform application must either not use the platform-specific functionality that changes from version to version, or use it very wisely in order to remain efficient under various runtimes.
Example 5. Implementation of standard classes
I will give one more instructive story. The guys from one good company once decided to use structures as keys for hash tables. Nothing foreshadowed trouble, the application worked fine. Slightly slowed down, but not so that straight is too critical. And then one day the guys decided to launch their application under Mono. And lo and behold: it began to work many times faster. “Oh, what a good Mono, how quickly everything works under it,” the guys were delighted. The only thing that did not give rest was the thought that it should not be so, that somewhere there was something wrong.
To understand this example, you need to read a little about the implementation of GetHashCode structures in MS.NET (
good habrapost ). In short, there are two versions for the hash function: one for structures without reference fields and free space between the fields, and the other for all others. As the thoughtful reader could guess, our guys were not doing well with the key structure (namely, there were “holes” between the fields; no one bothered to prescribe an explicit GetHashCode ()). And in this case, the second version of the hash function is used, which is based on the first field of the structure. Consider an example:
var a1 = new KeyValuePair<int, int>(1, 2); var a2 = new KeyValuePair<int, int>(1, 3); Console.WriteLine(a1.GetHashCode() == a2.GetHashCode()); var b1 = new KeyValuePair<int, string>(1, "x"); var b2 = new KeyValuePair<int, string>(1, "y"); Console.WriteLine(b1.GetHashCode() == b2.GetHashCode());
This code will print
False True
under MS.NET. The hashes
a1
and
a2
will be different (they will be calculated on the basis of all the fields), and the hashes for
b1
and
b2
will coincide (they will be considered only on the basis of the first field). Mono developers decided not to bother with a bunch of different versions of the hash function: they wrote one that works on the basis of all the fields (see
GetHashCode and
InternalGetHashCode ). Accordingly, the above code will print
False False
under Mono, because all the hashes will be different.
Let's go back to our funny guys with a hash table. If you look at the situation from the right angle, their application did not fly under Mono, but slowed down under MS.NET. And it slowed down because so many keys had the same hash due to the coinciding first field. It would seem a trifle, but it had a rather strong effect on performance.
You know, when I start telling someone about the .NET internals, I am often reproached with the phrases “Why should I know all this? Such details of internal implementations
will never be useful in real life. ” And I answer: "Well, well, of course, no one will ever come in handy."
.NEXT
I can persecute such stories for a very long time, but in this post I just wanted to identify common issues and get people to think again. Think about the fact that we live in a complex world in which your C # program may work differently depending on the environment. The difference in behavior rests on very specific things that can be sorted out and ensure that the .NET application works quickly and stably for different platforms.
If you want to listen to more funny stories, then come to my report at the
.NEXT conference (December 8, 2014, Moscow, Radisson Slavyanskaya). I will continue to discuss the topic of different platforms, talk about the features of Mono for working with memory and the differences in the implementations of JIT compilers. And I will wander around the site all day, so I can be caught and talked about .NET.
I also hope that Habrazhiteley has enough funny stories. I will be glad to hear them =)