nilaoda / N_m3u8DL-RE

Cross-Platform, modern and powerful stream downloader for MPD/M3U8/ISM. English/简体中文/繁體中文.
MIT License
4.71k stars 500 forks source link

Optimized HexToBytes function #469

Closed irodai-majom closed 3 weeks ago

irodai-majom commented 1 month ago

Used following bechmark to check the speedup:

Results are:

| Method              | N    | Mean         | Error      | StdDev     | Median       | Allocated |
|-------------------- |----- |-------------:|-----------:|-----------:|-------------:|----------:|
| HexToBytesSubstring | 100  |  1,132.54 ns |  22.490 ns |  47.440 ns |  1,110.33 ns |    3328 B |
| HexToBytesSlice     | 100  |    583.82 ns |  11.586 ns |  23.668 ns |    570.63 ns |     128 B |
| HexToBytesConvert   | 100  |     35.21 ns |   0.570 ns |   0.818 ns |     34.87 ns |     128 B |
| HexToBytesSubstring | 200  |  2,223.98 ns |  10.471 ns |   9.282 ns |  2,224.93 ns |    6624 B |
| HexToBytesSlice     | 200  |  1,154.18 ns |   5.493 ns |   4.587 ns |  1,152.19 ns |     224 B |
| HexToBytesConvert   | 200  |     61.24 ns |   0.875 ns |   0.731 ns |     60.86 ns |     224 B |
| HexToBytesSubstring | 1000 | 12,626.88 ns | 220.443 ns | 195.417 ns | 12,550.49 ns |   33024 B |
| HexToBytesSlice     | 1000 |  5,764.04 ns |  10.413 ns |   8.696 ns |  5,764.74 ns |    1024 B |
| HexToBytesConvert   | 1000 |    296.51 ns |   0.337 ns |   0.315 ns |    296.47 ns |    1024 B |

You can notice improvement in both execution time and memory allocated.

Benchmark code:

using System.Globalization;
using System.Text;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;

namespace N_m3u8DL-RE.Benchmark;

static class HexStingUtil
{
    public static byte[] HexToBytesSubstring(string hex)
    {
        hex = hex.Trim();
        if (hex.StartsWith("0x") || hex.StartsWith("0X"))
            hex = hex.Substring(2);
        byte[] bytes = new byte[hex.Length / 2];

        for (int i = 0; i < hex.Length; i += 2)
        {
            var hexDigit = hex.Substring(i, 2);
            bytes[i / 2] = Convert.ToByte(hexDigit, 16);
        }

        return bytes;
    }

    public static byte[] HexToBytesSlice(string hex)
    {
        var hexSpan = hex.AsSpan().Trim();
        if (hexSpan.StartsWith("0x") || hexSpan.StartsWith("0X"))
        {
            hexSpan = hexSpan.Slice(2);
        }

        byte[] bytes = new byte[hex.Length / 2];

        for (int i = 0; i < hex.Length; i += 2)
        {
            ReadOnlySpan<char> hexDigit = hexSpan.Slice(start: i, length: 2);
            var val = byte.Parse(hexDigit, NumberStyles.HexNumber);
            bytes[i / 2] = val;
        }

        return bytes;
    }

    public static byte[] HexToBytesConvert(string hex)
    {
        var hexSpan = hex.AsSpan().Trim();
        if (hexSpan.StartsWith("0x") || hexSpan.StartsWith("0X"))
        {
            hexSpan = hexSpan.Slice(2);
        }

        return Convert.FromHexString(hexSpan);
    }
}

[MemoryDiagnoser(false)]
public class HexStringToBytesBenchmark
{
    [Params(100, 200, 1000)] public int N;
    private string hexString;

    [GlobalSetup]
    public void Setup()
    {
        var buffer = new byte[N];
        new Random().NextBytes(buffer);

        hexString = Convert.ToHexString(buffer);
    }

    [Benchmark]
    public byte[] HexToBytesSubstring() => HexStingUtil.HexToBytesSubstring(hexString);

    [Benchmark]
    public byte[] HexToBytesSlice() => HexStingUtil.HexToBytesSlice(hexString);

    [Benchmark]
    public byte[] HexToBytesConvert() => HexStingUtil.HexToBytesConvert(hexString);
}

public class RunBenchmark
{
    public static void Main(string[] args)
    {
        var summary = BenchmarkRunner.Run<HexStringToBytesBenchmark>();
    }
}
nilaoda commented 3 weeks ago

Thx.