C# 性能 - 指向热循环中跨度的指针

C# performance - pointer to span in a hot loop

提问人:Yurii Palkovskii 提问时间:1/17/2023 最后编辑:Yurii Palkovskii 更新时间:1/18/2023 访问量:355

问:

我正在寻找BitConverter的更快替代品:

但!在“热循环”内部:

//i_k_size = 8 bytes 
while (fs.Read(ba_buf, 0, ba_buf.Length) > 0 && dcm_buf_read_ctr < i_buf_reads)
{
    Span<byte> sp_data = ba_buf.AsSpan();
    for (int i = 0; i < ba_buf.Length; i += i_k_size)
    {
        UInt64 k = BitConverter.ToUInt64(sp_data.Slice(i, i_k_size));
    }
 }

我努力将指针与转换集成 - 使性能变得更糟。指针可以用来更快地使用跨度吗?

以下是基准测试:指针 2 数组快 2 倍

实际上,我希望使用代码而不是BitConverter:

public static int l_1gb = 1073741824;
static unsafe void Main(string[] args)
{
    Random rnd = new Random();
    Stopwatch sw1 = new();
    sw1.Start();
    byte[] k = new byte[8];

    fixed (byte* a2rr = &k[0])
    {
        for (int i = 0; i < 1000000000; i++)
        {
            rnd.NextBytes(k);
            //UInt64 p1 = BitConverter.ToUInt64(k); 
            //time: 10203.824
            //time: 10508.981
            //time: 10246.784
            //time: 10285.889

            //UInt64* uint64ptr = (UInt64*)a2rr;
            //x2 performance !
            UInt64 p2 = *(UInt64*)a2rr;

            //time: 4609.814
            //time: 4588.157
            //time: 4634.494
        }
    }
    Console.WriteLine($"time: {Math.Round(sw1.Elapsed.TotalMilliseconds, 3)}");
}
C# 性能 循环 切片 BitConverter

评论

2赞 Jon Skeet 1/17/2023
顺便说一句,不一致的支撑、非常规的缩进和非常规的变量名称有点分散注意力——至少对我来说是这样,我怀疑其他人可能会有同样的感觉。
2赞 Jon Skeet 1/17/2023
“我认为大括号没有问题”——循环的“行尾大括号”与循环的“行首大括号”对你来说似乎并不一致吗?(至于“我希望你回答问题的重点”——如果我的建议有助于使你的问题对其他 10 个用户更具吸引力,那不是更有用吗?whilefor
2赞 madreflection 1/17/2023
是的,它“预设”了它。这很清楚。但是,如果你没有得到你所期望的,你就没有错误检查,所以它会悄悄地给你坏数据。
3赞 madreflection 1/18/2023
你怎么知道?如果返回大于但小于 ,则您不知道,因为您尚未保存读取的实际字节数。fs.Read0ba_buf.Length
3赞 madreflection 1/18/2023
想象一下,如果你在我第一次发表评论后这样做,效率会更高。还有 Blindy's。

答:

5赞 Blindy 1/17/2023 #1

假设是一个 ,运行循环的一种非常简单和有效的方法是:ba_bufbyte[]

foreach(var value in MemoryMarshal.Cast<byte, ulong>(ba_buf))
   // work with value here 

如果您需要对缓冲区进行微调(例如,切断其中的一部分),请先使用它。AsSpan(start, count)

评论

0赞 Yurii Palkovskii 1/18/2023
1073741824转换比 BitConverter.ToUInt64 快 4 秒(我的测试设置)。仍然 - 我不确定这是否比指针跨度更快(如果可能的话)
0赞 Yurii Palkovskii 1/18/2023
我添加了一个 BM 代码来支持我的 oppinion。
2赞 Blindy 1/18/2023
不需要意见,可以直接检查代码。这是最快的。任何其他性能问题都来自有问题的决策,例如对非常大的文件使用流,尤其是对于微小的读取。
0赞 Yurii Palkovskii 1/18/2023
哇!在线disasm太棒了!以前从未见过!谢谢你的链接!
2赞 Matthew Watson 1/18/2023 #2

你可以通过在读取循环之外初始化一些跨度,然后直接读入一个并通过一个访问数据来优化这一点,如下所示:Span<byte>Span<ulong>

int buf_bytes = sizeof(ulong) * 1024; // Or whatever buffer size you need.
var ba_buf    = new byte[buf_bytes];
var span_buf  = ba_buf.AsSpan();
var data_span = MemoryMarshal.Cast<byte, ulong>(span_buf);

while (true)
{
    int count = fs.Read(span_buf) / sizeof(ulong);

    if (count == 0)
        break;

    for (int i = 0; i < count; i++)
    {
        // Do something with data_span[i]

        Console.WriteLine(data_span[i]); // Put your own processing here.
    }
}

这样可以尽可能避免内存分配。当它的数据用完时,它会终止读取循环,如果返回的字节数不是它的倍数,则忽略多余的字节。sizeof(ulong)

它将始终读取所有可用数据,但如果您想提前终止它,您可以添加代码来执行此操作。

例如,考虑以下代码,它将 2,000 个 ulong 值写入文件,然后使用上面的代码将它们读回:

using (var output = File.OpenWrite("x"))
{
    for (ulong i = 0; i < 2000; ++i)
    {
        output.Write(BitConverter.GetBytes(i));
    }
}

using var fs = File.OpenRead("x");

int buf_bytes = sizeof(ulong) * 1024; // Or whatever buffer size you need.
var ba_buf    = new byte[buf_bytes];
var span_buf  = ba_buf.AsSpan();
var data_span = MemoryMarshal.Cast<byte, ulong>(span_buf);

while (true)
{
    int count = fs.Read(span_buf) / sizeof(ulong);

    if (count == 0)
        break;

    for (int i = 0; i < count; i++)
    {
        // Do something with data_span[i]

        Console.WriteLine(data_span[i]); // Put your own processing here.
    }
}

评论

1赞 Yurii Palkovskii 1/18/2023
对不起 - 起初,我错过了您代码中最重要的一点 - data_span + MemoryMarshal.Cast 我已经测试并编辑了您的方法 - 它是迄今为止2147483648迭代中性能最高的 bm: 1. 我的 BitConverter 代码 = 192 秒 2.foreach(var value in MemoryMarshal.Cast<byte, ulong>(ba_buf)) = 187 秒 3.MemoryMarshal.Cast + 外部缓冲区跨度(您的代码) 183 秒 非常感谢!