Live data from Hacker News

Decompiling C# (async/await)

community.sharpdevelop.net

21–29 of 29 posts

Re: Decompiling C# (async/await)

#21

About the only time that you have to be really careful with stuff like this is if you're writing something that's super sensitive to GC pauses, such as an XNA game. In those cases, yield return, linq, lambdas, some foreach loops will all generate short lived objects that will cause the GC to kick in more often. So if you're doing that in every update loop you could end up with performance issues. And even that is onl…

The C# compiler one-ups F# here, and will cache the delegate for lambdas, so long as they don't capture any locals (that is if it's "lifted") - so lambdas don't necessarily mean an extra object. (Although the LINQ methods need enumerables and enumerators.)

When did they introduce caching for lambdas? I've been caching them by hand since I used to see them pop up in CLR Profiler all the time. Is it unable to cache lambdas constructed in member functions because it can't be sure they don't close over 'this'?

Re: Decompiling C# (async/await)

#22

The ILSpy team's hard work is part of what made it possible for me to write my .NET -> JS compiler ( http://jsil.org/ ). My ~120k LoC wouldn't work without their ~450k LoC (well, I don't consume all 450k...) ILSpy is a pretty interesting application/library to look at under the hood. The decompilation logic that transforms .NET bytecode (MSIL) into higher-level data structures is split into a bunch of well-defined tr…

Interesting, so the benefit there is generating higher-level JavaScript instead of interpreting IL at a lower level?

Yeah. I consume the munged IL that comes out of their transform pipeline (though for complex reasons, I don't use all of it - some of their transforms are destructive in ways that aren't helpful, or I'd have to undo them) which saves me the trouble of reimplementing things they already figured out, like how to transform most branch/jump patterns into if statements and while loops.

I could generate JS from raw IL (and other projects like Volta did just that) but ILSpy gives me a huge head start in terms of producing JS that actually looks like what you'd write by hand. For loops instead of while loops, switch statements instead of cascading ifs, etc.

Re: Decompiling C# (async/await)

#23

About the only time that you have to be really careful with stuff like this is if you're writing something that's super sensitive to GC pauses, such as an XNA game. In those cases, yield return, linq, lambdas, some foreach loops will all generate short lived objects that will cause the GC to kick in more often. So if you're doing that in every update loop you could end up with performance issues. And even that is onl…

Reminds me of an experience I had. I kind of naively wrote something as a method that would "yield return" bytes. After all, everyone is familiar with that attitude so many people have, that you write what looks nicest and worry about bottlenecks later. I'm personally not usually too big on that attitude (I think it's often an overused excuse for obviously bad code) but "yield return" does let you write some very nat…

A "yield return enumerable" is "delay executed" so evaluating it multiple times causes the yield-return execution to occur at every evaluation (if you haven't taken care to ToArray() or ToList() it). Do something like:

  var myBytes = GetMyBytesItr();
  for (var i = 0; i 
and you'll be in a world of hurt especially if GetMyBytesItr() allocates a memory buffer. Count() causes the "get the bytes data" action to occur, as does every iteration of ElementAt(). Now I'm not saying this is definitely what you were experiencing, but it's a common pitfall. Also, using ElementAt() for each iteration is, of course, completely contrived for this example (you'd want to foreach instead which would cause only one execution of the "get the bytes data" action).

Re: Decompiling C# (async/await)

#24

Earlier quoted context omitted.

Reminds me of an experience I had. I kind of naively wrote something as a method that would "yield return" bytes. After all, everyone is familiar with that attitude so many people have, that you write what looks nicest and worry about bottlenecks later. I'm personally not usually too big on that attitude (I think it's often an overused excuse for obviously bad code) but "yield return" does let you write some very nat…

A "yield return enumerable" is "delay executed" so evaluating it multiple times causes the yield-return execution to occur at every evaluation (if you haven't taken care to ToArray() or ToList() it). Do something like: var myBytes = GetMyBytesItr(); for (var i = 0; i and you'll be in a world of hurt especially if GetMyBytesItr() allocates a memory buffer. Count() causes the "get the bytes data" action to occur, as do…

That wasn't the issue in my case. It was foreach (var b in f()). f did not do any allocations, just a loop with a bunch of yield return statements.

This is why I suspected that it was simply worse machine code after JIT. But I wasn't sure of all the details of the code that was inserted on my behalf.

Re: Decompiling C# (async/await)

#25

Earlier quoted context omitted.

The C# compiler one-ups F# here, and will cache the delegate for lambdas, so long as they don't capture any locals (that is if it's "lifted") - so lambdas don't necessarily mean an extra object. (Although the LINQ methods need enumerables and enumerators.)

When did they introduce caching for lambdas? I've been caching them by hand since I used to see them pop up in CLR Profiler all the time. Is it unable to cache lambdas constructed in member functions because it can't be sure they don't close over 'this'?

It's been years, at least - maybe from the beginning? I just tried a small program with a lambda inside a member function. Works fine, generates IL like:

  IL_0001:  ldarg.0
  IL_0002:  ldsfld     class [mscorlib]System.Func`2 test.Program::'CS$9__CachedAnonymousMethodDelegate1'
  IL_0007:  brtrue.s   IL_001c
  // create and store delegate
  IL_001c:  Load delegate from field and call
   
F# will do neat stuff like completely eliminate the lambda, if you're only using it locally. It even does some constant detection across functions, so it can calculate constant functions at compile time. But if not, F# will create a new closure object every time.

Re: Decompiling C# (async/await)

#26

Earlier quoted context omitted.

Interesting, so the benefit there is generating higher-level JavaScript instead of interpreting IL at a lower level?

Yeah. I consume the munged IL that comes out of their transform pipeline (though for complex reasons, I don't use all of it - some of their transforms are destructive in ways that aren't helpful, or I'd have to undo them) which saves me the trouble of reimplementing things they already figured out, like how to transform most branch/jump patterns into if statements and while loops. I could generate JS from raw IL (and…

Volta, from the demo I used a long time ago, seemed horrendously slow, too. JSIL feels far faster. I guess you gain a bit of performance by making the code higher level so the JS engines can tell if an optimization is safe.

Is JSIL limited to a subset of IL? Can you target C++ (in pure mode) to it? Opcodes like cpblk, and others?

Re: Decompiling C# (async/await)

#27

Earlier quoted context omitted.

A "yield return enumerable" is "delay executed" so evaluating it multiple times causes the yield-return execution to occur at every evaluation (if you haven't taken care to ToArray() or ToList() it). Do something like: var myBytes = GetMyBytesItr(); for (var i = 0; i and you'll be in a world of hurt especially if GetMyBytesItr() allocates a memory buffer. Count() causes the "get the bytes data" action to occur, as do…

That wasn't the issue in my case. It was foreach (var b in f()). f did not do any allocations, just a loop with a bunch of yield return statements. This is why I suspected that it was simply worse machine code after JIT. But I wasn't sure of all the details of the code that was inserted on my behalf.

One of the biggest differences is that if you do:

  foreach (var byte in byteArray)
The compiler transforms that into a normal for-loop that accesses the array directly. So that's already an advantage for the array over your enumerator.

The other difference that will have a major performance impact is the way the IEnumerator pattern works, even with generics. For:

  foreach (var b in f())
The generated code looks roughly like this:

  using (var enumerator = f())
  while (enumerator.MoveNext()) {
    var byte = enumerator.get_Current();
  }
As a result, you've gone from 0 method calls per iteration (direct array access) to 2 virtual method calls per iteration (MoveNext and get_Current). An incredibly smart JIT might be able to figure out that the virtual methods are always the same and turn them into static invocations, or even inline them, but I don't think the CLR can do this for you reliably.

Re: Decompiling C# (async/await)

#28

About the only time that you have to be really careful with stuff like this is if you're writing something that's super sensitive to GC pauses, such as an XNA game. In those cases, yield return, linq, lambdas, some foreach loops will all generate short lived objects that will cause the GC to kick in more often. So if you're doing that in every update loop you could end up with performance issues. And even that is onl…

Reminds me of an experience I had. I kind of naively wrote something as a method that would "yield return" bytes. After all, everyone is familiar with that attitude so many people have, that you write what looks nicest and worry about bottlenecks later. I'm personally not usually too big on that attitude (I think it's often an overused excuse for obviously bad code) but "yield return" does let you write some very nat…

A for loop will have bounds checking removed, for one. Also, the JIT does better inside single method bodies. Compare:

  1: for(i = 0; i 
In the first case, you end up with a small, relatively tight loop (the machine code has a lot of extra stuff I don't quite understand).

In the second case, you're literally doing a virtual call (and I don't think the CLR inlines interface calls) to get_Current() in a loop, followed by MoveNext(). So there's 2 function call overheads, not to mention the actual code that get_Current and MoveNext have.

Here's the sample program[1]. I get about 600% runtime for the enumerable version versus the array. Given all the extra work, I'm sorta impressed it's only 6x. The 32-bit JIT is a bit slower doing the array method than the 64-bit, which surprises me. Here's the code for the loops[2].

1: http://pastebin.com/GAg3RiNx

2: http://pastebin.com/index/a1eC8sX9

Re: Decompiling C# (async/await)

#29

Earlier quoted context omitted.

Yeah. I consume the munged IL that comes out of their transform pipeline (though for complex reasons, I don't use all of it - some of their transforms are destructive in ways that aren't helpful, or I'd have to undo them) which saves me the trouble of reimplementing things they already figured out, like how to transform most branch/jump patterns into if statements and while loops. I could generate JS from raw IL (and…

Volta, from the demo I used a long time ago, seemed horrendously slow, too. JSIL feels far faster. I guess you gain a bit of performance by making the code higher level so the JS engines can tell if an optimization is safe. Is JSIL limited to a subset of IL? Can you target C++ (in pure mode) to it? Opcodes like cpblk, and others?

Volta's approach seems to be essentially implementing a low level .NET runtime and representing the bytecode as JS. It gives you some cool stuff for free (for example, that low level approach means their type system works almost exactly like .NET's universally) but it does indeed mean that you have to work harder to give JS engines an opportunity to optimize your code. It makes integration tougher, too. In comparison, the best way to describe JSIL's approach is trying to express .NET concepts on top of JS, so it uses JS standard libraries and types wherever possible.

JSIL is theoretically limited in that there are things expressible in IL that you simply can't do in a browser. However, out of all the executables I've run the compiler on so far, very little of the IL they contain is actually impossible to translate - the tricky patterns and opcodes seem to get used only occasionally in one or two methods.

Some parts are definitely harder than others; I've only recently gotten support for pointers and the 'unsafe' class of C# features working: http://jsil.org/try/#5055026 and that's only covering a subset of all the different opcodes defined for doing interesting things with pointers and references. For example, function pointers will probably never work, and IIRC there are a few opcodes dedicated to interacting with those.

Post reply on HN