Skip to content

Added ReadOnlySpan<char> APIs and moved the throws into a cold section#54

Merged
CarlVerret merged 1 commit into
CarlVerret:masterfrom
gfoidl:ros-api
Feb 25, 2021
Merged

Added ReadOnlySpan<char> APIs and moved the throws into a cold section#54
CarlVerret merged 1 commit into
CarlVerret:masterfrom
gfoidl:ros-api

Conversation

@gfoidl

@gfoidl gfoidl commented Feb 24, 2021

Copy link
Copy Markdown
Contributor

Fixes #50

private void TestGeneral_Float(string sut, float expected_value)
{
Assert.Equal(expected_value, FastFloatParser.ParseFloat(sut));
Assert.Equal(expected_value, FastFloatParser.ParseFloat(sut.AsSpan()));

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ReadOnlySpan path is almost the same as for string, so I think just these tests are enough.

if (s == null)
throw new ArgumentNullException(nameof(s));
ThrowArgumentNull();
static void ThrowArgumentNull() => throw new ArgumentNullException(nameof(s));

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is C#'s local function. With these the throw can be moved to a (non-inlineable) method on the cold side. So the actual methods gets smaller. See this sharplab on how it looks like.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So the actual methods gets smaller.

Can you elaborate on the benefits ? (I am not being argumentative, it is an honest question. Your code is quite certainly fine.)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Smaller code is almost always preferable, as the I-cache can be utilized better.
With the "inline" throw new the JIT moves the exception-part to the end of the generated code, but it's quite a bit of ceremony needed to setup the exception and throw it.
With the "throw-helper"-trick the exception-part is moved into a separate method, thus the "working"-part of the method gets smaller. As exceptions are exceptional and in the case of an exception the additional indirection won't harm.
This is a "trick" we use quite a lot in the .NET runtime (but only on really hot-path methods).

For a simple example the difference is

InlineThrow
    L0000: push rsi
    L0001: sub rsp, 0x30
    L0005: xor eax, eax
    L0007: mov [rsp+0x28], rax
    L000c: test rdx, rdx
    L000f: je short L0028
    L0011: add rdx, 0xc
    L0015: mov [rsp+0x28], rdx
    L001a: mov rax, [rsp+0x28]
    L001f: movzx eax, word ptr [rax]
    L0022: add rsp, 0x30
    L0026: pop rsi
    L0027: ret
    L0028: mov rcx, 0x7ffe9cff3640
    L0032: call 0x00007ffefcaaa370
    L0037: mov rsi, rax
    L003a: mov ecx, 1
    L003f: mov rdx, 0x7ffea483c080
    L0049: call 0x00007ffefc9b0b70
    L004e: mov rdx, rax
    L0051: mov rcx, rsi
    L0054: call System.ArgumentNullException..ctor(System.String)
    L0059: mov rcx, rsi
    L005c: call 0x00007ffefca80b00
    L0061: int3
ThrowHelper
    L0000: sub rsp, 0x28
    L0004: xor eax, eax
    L0006: mov [rsp+0x20], rax
    L000b: test rdx, rdx
    L000e: je short L0026
    L0010: add rdx, 0xc
    L0014: mov [rsp+0x20], rdx
    L0019: mov rax, [rsp+0x20]
    L001e: movzx eax, word ptr [rax]
    L0021: add rsp, 0x28
    L0025: ret
    L0026: call C.<M1>g__ThrowArgumentNull|1_0()
    L002b: int3

If in a future version of the JIT fixed doesn't prevent inlining the benefit will be even bigger.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok. So the binary will still end up being about the same size, but JIT-compiled version will have more clearly identifiable "cold regions" which is evidently beneficial. But this implies that the compiler can't do this on its own.

If I compile C++ code like this:

int square(int num) {
    if(num == 0) throw std::runtime_error("fsd");
    return num * num;
}

I always expected that the throw would jump to some cold region of the binary code and do its thing.

I can't verify that C++ do this... even though it seems like an obvious optimization. Instead, you seem to get code that looks like the C# assembly... the exception handling is interleaved with the exception-free code.

If in a future version of the JIT fixed doesn't prevent inlining the benefit will be even bigger.

Are you saying that if a function uses fixed, then it can't be inlined? If so, I'd really like to know.

(Please again note that I am not being argumentative. My curiosity is genuine.)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this implies that the compiler can't do this on its own.

Current (.NET 5) version of the JIT doesn't do this. But there is some on-going work to make this better. Maybe even C# will move things around...it's not clear at the moment. Will see...
I also expect this to be an obvious optimization.

Are you saying that if a function uses fixed, then it can't be inlined?

I re-checked things, and have to revert this statement a bit.
The JIT is sometimes able to inline methods with fixed expressions, attributing the method with AggressiveInlining makes it more likely to do so.
.NET has to ensure that a fixed expression (= pinning) gets unpinned no matter how control leaves the scope of fixed -- conceptionally it's enclosed in a try-finally. So things may become complicated and with the limited time-bugdet among other constraints that a JIT has, the pattern on when a method with fixed gets inlined isn't -- at least for me -- very predictable. Best is to look at the generated asm and JIT-dumps to see what's happening.

As a rule of thumb one should use fixed as early as possible, so that inner / hot code can work on the already fixed variables. Excately as done in this project.


(Please again note that I am not being argumentative. My curiosity is genuine.)

Absolutely no problem. As a long-term follower of your blog it's a honor to have a discussion with you 😄

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(I'm not registered on twitter, so a further note here)

In .NET methods that never return (e.g. throw) won't be inlined. By splitting the exception-part into a separeate method, the working-part will get eligible for inlining.*

* if actually inlined or not depends on a plethora of factors.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@gfoidl Throwing methods do not get inlined. Ah. That's a major argument against throwing within small methods then!!!

@lemire

lemire commented Feb 25, 2021

Copy link
Copy Markdown
Collaborator

@CarlVerret I recommend merging. It would be nice to update the README to document this part of the API but it can be done separately.

@CarlVerret
CarlVerret merged commit c28245d into CarlVerret:master Feb 25, 2021
@gfoidl
gfoidl deleted the ros-api branch February 25, 2021 16:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Provide ReadOnlySpan<char> API

3 participants