3 ms·
The call to "memcmp" has overhead. It's an imported function which cannot be inlined, and the marshaller will automatically create pinned GC handles to the mem
by Dwedit 2y ago
The call to "memcmp" has overhead. It's an imported function which cannot be inlined, and the marshaller will automatically create pinned GC handles to the memory inside of the arrays as they are passed to the native code.
I wonder how it would compare if you passed actual pointers to "memcmp" instead of marshalled arrays. You'd use "fixed (byte *p = bytes) {" on each array first so that the pinning happens outside of the function call.
- deleted 2y ago[deleted]
- xnorswap 2y agoI tried that but cut it from the code because it had the same performance.
- Rohansi 2y agoHave you tried the newer [LibraryImport] attribute?
- xnorswap 2y agoI haven't, I wasn't aware of that attribute. I would gratefully accept a PR with such a benchmark case. Edit: I've now tried it, and it reduced overhead a small amount. (e.g. Average 7.5 ns vs 8 ns for the 10-byte array )
- MarkSweep 2y agoI'm pretty sure the marshaling code for the pinvoke is not creating GC handles. It is just using a pinned local, like a fixed statement in csharp does. This is what the LibraryImport at least and I don't see why the built in marshaller would be different. The author says in the peer comment that they confirmed the performance is the same. I think the blog post is quite good at showing that seemingly similar things can have different performance tradeoffs. A follow up topic might digging deeper into the why. For example, if you look at the disassembly of the p/invoke method, you can see the source of the overhead: setting up a p/invoke frame so the stack is walkable while in native code, doing a GC poll after returning from the native function, and removing the frame. https://gist.github.com/AustinWise/21d518fee314ad484eeec981aeed78f1 https://gist.github.com/AustinWise/21d518fee314ad484eeec981a...
- mhh__ 2y agomemcmp and friends can be a funny one when looking at disasm Depending on context and optimization settings we might see: - Gone entirely - A memcmp call has been inlined and turned into a single instruction - It's turned into a short loop - A loop has been turned into a memcmp call. FWIW This is also one of the reasons why I think the VM-by-default / JIT way holds dotnet back. I find it very hard to be confident about what the assembly actually looks like, and after that. Subtly I think it also encourages a "that'll do" mindset up the stack. You're working in an environment where you're not really incentivised to care so some patterns just don't feel like they'd have happened in a more native language.
- int_19h 2y agoFor what it's worth, I have read .NET JIT disassembly as part of perf work on a couple of occasions. On Windows, at least, Visual Studio enables this seamlessly - if you break inside managed code, you can switch to Disassembly view and see the actual native code corresponding to each line, step through it etc.
- neonsunset 2y ago> I find it very hard to be confident about what the assembly actually looks like, and after that. Godbolt is your friend as a DPGO-less baseline. Having JIT is an advantage w.r.t. selecting the best SIMD instruction set. > Subtly I think it also encourages a "that'll do" mindset up the stack. What is the basis for this assumption?
- mhh__ 2y ago> Having JIT is an advantage w.r.t. selecting the best SIMD instruction set. On paper yes but does anyone really rely on it? multiversioning is easy to do in a aot model too and even then most people don't bother. obviously sometimes its critical. The more magic you put into the jit also makes it slower, so even though there are _loads_ of things you can do with a good JIT a lot them don't actually happen in practice. PGO is one of those things. I've never really encountered it in dotnet but it is basically magic in frontend-bound programs like compilers. > What is the basis for this assumption? It's not an assumption, it's my impression of the dotnet ecosystem. I do think also some patterns somewhat related to JITed-ness has led to some patterns (particularly around generics) that mean that common patterns in the language can't actually be expressed statically so one ends up with all kinds of quasi-dynamically typed runtime patterns e.g. dependency injection. But this is more of a design decision that comes from the same place.