jdk8字符串变为long集合_ASP.NET Core CorrelationId 字符串生成性能优化

原文 Some performance tricks with .NET strings

作者给 AspNetCore 提交了 Pull Request,针对相关字符串 Id 生成做了优化,优化后的代码比原来性能快一倍多。

原始代码

代码将一个 long 值编码为 base 32 的字符串。而且这段代码已经经过优化,优化方式可见注释。

private static readonly string s_encode32Chars = "0123456789ABCDEFGHIJKLMNOPQRSTUV";

public static unsafe string GenerateId(long id)
{
    // The following routine is ~310% faster than calling long.ToString() on x64
    // and ~600% faster than calling long.ToString() on x86 in tight loops of 1 million+ iterations
    // See: https://github.com/aspnet/Hosting/pull/385

    // stackalloc to allocate array on stack rather than heap
    char* buffer = stackalloc char[13];
    buffer[0] = s_encode32Chars[(int)(id >> 60) & 31];
    buffer[1] = s_encode32Chars[(int)(id >> 55) & 31];
    buffer[2] = s_encode32Chars[(int)(id >> 50) & 31];
    buffer[3] = s_encode32Chars[(int)(id >> 45) & 31];
    buffer[4] = s_encode32Chars[(int)(id >> 40) & 31];
    buffer[5] = s_encode32Chars[(int)(id >> 35) & 31];
    buffer[6] = s_encode32Chars[(int)(id >> 30) & 31];
    buffer[7] = s_encode32Chars[(int)(id >> 25) & 31];
    buffer[8] = s_encode32Chars[(int)(id >> 20) & 31];
    buffer[9] = s_encode32Chars[(int)(id >> 15) & 31];
    buffer[10] = s_encode32Chars[(int)(id >> 10) & 31];
    buffer[11] = s_encode32Chars[(int)(id >> 5) & 31];
    buffer[12] = s_encode32Chars[(int)id & 31];
    return new string(buffer);
}

使用 Span<T>

第一个 commit 是用 Span<T> 来取代原来的 unsafe 代码。基本对性能没影响,但不再是 unsafe 了。

private static readonly string s_encode32Chars = "0123456789ABCDEFGHIJKLMNOPQRSTUV";

// Remove unsafe keyword
public static string GenerateId(long id)
{
    // Replace char* with Span<T>
    Span<char> buffer = stackalloc char[13];
    buffer[0] = s_encode32Chars[(int)(id >> 60) & 31];
    buffer[1] = s_encode32Chars[(int)(id >> 55) & 31];
    ...
    buffer[11] = s_encode32Chars[(int)(id >> 5) & 31];
    buffer[12] = s_encode32Chars[(int)id & 31];
    return new string(buffer, 0, 13);
}

使用 string.Create

第一个优化是使用 string.Create。在上面的代码,我们在 stack 上创建了一个 buffer,然后再使用这个 buffer 创建了一个在 heap 上的 string。这意味着 buffer 被复制到了新的字符串实例。我们可以使用 string.Create 来避免这个复制过程。这个方法直接在 heap 上分配字符串,而且你能过够使用 delegate 来直接设置字符串的内容。参见 Github 上的源码。

private static readonly string s_encode32Chars = "0123456789ABCDEFGHIJKLMNOPQRSTUV";

public static string GenerateId(long id)
{
    return string.Create(13, id, (buffer, value) =>
    {
        // Do not use "id" here as it would create a closure and allocate
        // instead use "value" (read hereafter for more details)
        buffer[0] = s_encode32Chars[(int)(value >> 60) & 31];
        buffer[1] = s_encode32Chars[(int)(value >> 55) & 31];
        ...
        buffer[11] = s_encode32Chars[(int)(value >> 5) & 31];
        buffer[12] = s_encode32Chars[(int)value & 31];
    }
}

你可能注意到我没有在 lambda 表达式中使用 id。因为这样会导致闭包。闭包在这里起到坏作用,会让 delegate 无法缓存,每次这个方法被调用都会实例化一个新的 delegate。最终会拖慢性能还给 GC 增加了压力。这就是为什么 string.Create 的第二个参数 TState state 会存在的原因。这个参数让我们可以避免使用闭包。你可以在 the Jetbrains'blog 了解更多关于闭包的信息。

使用 string.Create 在基准测试中快了大约 35% !

Steve Gordon 也写过一篇关于 string.Create 的文章:Creating Strings with No Allocation Overhead Using String.Create

反转赋值顺序

David Fowler 建议我把赋值的顺序倒置过来(譬如先赋值 buffer[12] 然后 buffer[11] 依此类推)。这样使 JIT 不再添加 bounds checks。毕竟,如果你可以访问 buffer[12],更小的索引值就更不可能越界了。

private static readonly string s_encode32Chars = "0123456789ABCDEFGHIJKLMNOPQRSTUV";

public static string GenerateId(long id)
{
    return string.Create(13, id, (buffer, value) =>
    {
        // Assign buffer from 12 to 0 to avoid a bound check
        buffer[12] = s_encode32Chars[(int)value & 31];
        buffer[11] = s_encode32Chars[(int)(value >> 5) & 31];
        ...
        buffer[1] = s_encode32Chars[(int)(value >> 55) & 31];
        buffer[0] = s_encode32Chars[(int)(value >> 60) & 31];
    }
}

JIT 生成优化后的代码后,这优化后的版本在我的机器上的确跟上一个性能一模一样。 Günther Foidl 提了个看法,这可能是由于 CPU 的分支预测在这种情况下工作得非常好。这不意味着这个优化全无作用,其他的 CPU 可能不会对这种情况有那么好的分支预测。而且,新生成的代码更加少,总是件好事。

Tip:使用 BenchmarkDotNet 你可以很容易通过使用 [DisassemblyDiagnoser(printAsm: true, printSource: true)] 声明得到汇编代码。

如下是优化后的汇编代码的对比:

264ba870206e658d050591ccaa0b0eb8.png

全部汇编代码在这里。

复制字段到本地变量

JIT 必须针对每次访问 s_encode32Chars 生成加载代码,因为它是一个可变的数组(它的引用是 readonly 的,但其中的单个元素不是不可能改变的)因此从 JIT 的角度来看其中的元素可能会改变,所以每次必须加载最新的数据。

这可以通过在本地加载它一次来避免,这样 JIT 可以跟踪到没有任何改变发生,可以实现只读的访问。

private static readonly string s_encode32Chars = "0123456789ABCDEFGHIJKLMNOPQRSTUV";

public static string GenerateId(long id)
{
    return string.Create(13, id, (buffer, value) =>
    {
        // Copy the reference
        var encode32Chars = s_encode32Chars;

        buffer[12] = encode32Chars[(int)value & 31];
        buffer[11] = encode32Chars[(int)(value >> 5) & 31];
        ...
        buffer[1] = encode32Chars[(int)(value >> 55) & 31];
        buffer[0] = encode32Chars[(int)(value >> 60) & 31];
    }
}

基准测试表明这个版本比上个版本快 4% !

b39381979973f52035323f4a4690148d.png

移除显式的类型转换

最后一个优化是移除显式的到 int 的类型转换。string 的索引器只接受 int,所以需要做这样的转换。但我们可以很简单地将 string 变为 char[],这样我们就可以使用 long 来索引。去掉这个转换后,JIT 可以为 64 位系统生成更优的代码。对于 32 位系统,会生成附加的代码,从而导致较差的性能。但鉴于绝大多数的应用现在是在 64 位系统上面跑(尤其是 Web Server),所以这样的改动是完全可接受的。

// Change from string to char[], so we can use the long
private static readonly char[] s_encode32Chars = "0123456789ABCDEFGHIJKLMNOPQRSTUV".ToCharArray();

public static string GenerateId(long id)
{
    return string.Create(13, id, (buffer, value) =>
    {
        var encode32Chars = s_encode32Chars;

        // Remove explicit cast in the indexer
        buffer[12] = encode32Chars[value & 31];
        buffer[11] = encode32Chars[(value >> 5) & 31];
        ...
        buffer[1] = encode32Chars[(value >> 55) & 31];
        buffer[0] = encode32Chars[(value >> 60) & 31];
    }
}

基准测试表明现在代码比上一个版本快大约 5% !

基准测试

我使用 BenchmarkDotNet 创建了一个基准测试来测量这个 pull requst 的每次改动会造成怎样的性能变化。你可以在这里找到代码:The code on Gist

测量结果

最终代码比最开始的代码快一倍,而且不再有 unsafe 代码。感谢 pull request 的所有 reviewer 帮助我发现新的 C# features!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值