リファレンスコードは遅い(BitFlyer-API)

以下の内容は.Net7.0で検証しています。
古い環境は私はあまり気にしていませんので、悪しからず。

前から思っていたことではあるのですが、リファレンスコードってなんで遅いんだろう。
いや、わかりやすい事が最優先課題ってことなんでしょうね。

今回、BitFlyerを例に挙げてみます。
あ、私が使っている仮想通貨マーケットの中で、BitFlyerのAPIは結構良い方だと思っています。
BitFlyerが悪いって話ではありませんので。

今回のは、BitFlyerの、sendchildorder のAPIです。

C#のコードを見てみてください。

まず、小さい点ですが、methodが気になります。
stringですね。

var method = "POST";
// ...
var request = new HttpRequestMessage(new HttpMethod(method), path + query);

//↓

var method = HttpMethod.Post;
// ...
var request = new HttpRequestMessage(method, path + query);

こんな感じに書き換えるだけでちょっと早くなりますね。
HttpMethod.Postは、ライブラリの中で事前にstaticで作ってキャッシュしてくれていますので、コストが少ないです。

次に、MediaTypeは都度作らず、キャッシュしたものを使いまわした方がいいでしょう。

content.Headers.ContentType = new MediaTypeHeaderValue("application/json");

//↓

static MediaTypeHeaderValue _mediaType = new MediaTypeHeaderValue("application/json");
// ...
content.Headers.ContentType = _mediaType;

次に気になった点として、SignWithHMACSHA256 メソッドの中身の encoderです。
以前に書きましたが、事前に作ってキャッシュする。使うときにlockした方が速いです。

static string SignWithHMACSHA256(string data, string secret)
{
    using (var encoder = new HMACSHA256(Encoding.UTF8.GetBytes(secret)))
    {
        var hash = encoder.ComputeHash(Encoding.UTF8.GetBytes(data));
        return ToHexString(hash);
    }
}

//↓

static HMACSHA256 _hmac = new HMACSHA256(Encoding.UTF8.GetBytes(apiSecret));

static string SignWithHMACSHA256New(string data)
{
    byte[] hash;
    lock(_hmac)
    {
        hash = _hmac.ComputeHash(Encoding.UTF8.GetBytes(data));
    }
    return ToHex_Lower(hash);
}

次に、ToHexStringメソッドです。

static string ToHexString(byte[] bytes)
{
    var sb = new StringBuilder(bytes.Length * 2);
    foreach (var b in bytes)
    {
        sb.Append(b.ToString("x2"));
    }
    return sb.ToString();
}

わかりやすいのは素晴らしいのですが、これは遅いですね。
特に、b.ToString(“x2”) この部分。byte[]の長さの分だけ、stringオブジェクト作っちゃう。
これを書き換えると、

public static string ToHex_Lower(ReadOnlySpan<byte> bytes)
{
    const string HexValues = "0123456789abcdef";

    Span<char> chars = stackalloc char[bytes.Length * 2];

    for (int i = 0; i < bytes.Length; i++)
    {
        var b = bytes[i];
        chars[i * 2] = HexValues[b >> 4];
        chars[i * 2 + 1] = HexValues[b & 0xF];
    }

    return new string(chars);
}

こんな感じでしょうか。
ちょとベンチマークを取ってみたら、20byteのデータで、405ns→39nsと10倍くらい速くなっていました。
これらを修正したところでベンチを取ってみました。

|    Method |        Mean |     Error |    StdDev |
|---------- |------------:|----------:|----------:|
| Reference | 2,417.59 ns | 39.546 ns | 35.057 ns |
|    Update | 1,127.76 ns | 13.464 ns | 11.243 ns |

計測したのは、送信の直前までです。
速度が2倍以上にはなっていますね。
まあ、マイクロ秒 レベルの話ですので、ネットワーク回線が遅かったりするインパクトに比べると、小さな話なんでしょうが。
でも、書き換えておくだけで、速くなるのでお勧めです。

ちなみに、本当に速くしたい場合、stringを極力排除して、全体をutf8で一貫して処理できるようにすると、もっと早くなります。
その場合、StringContentは使わずに、ByteArrayContentを使うことになります。

下に、ベンチマークを実行したコードを載せておきます。

static readonly Uri endpointUri = new Uri("https://api.bitflyer.com");
static readonly string apiKey = "{{ YOUR API KEY }}";
static readonly string apiSecret = "{{ YOUR API SECRET }}";

[Benchmark]
public async Task<(HttpClient, HttpRequestMessage)> Reference()
{
    var method = "POST";
    var path = "/v1/me/sendchildorder";
    var query = "";
    var body = @"";

    //using (var client = new HttpClient())
    //using (var request = new HttpRequestMessage(new HttpMethod(method), path + query))
    //using (var content = new StringContent(body))
    var client = new HttpClient();
    var request = new HttpRequestMessage(new HttpMethod(method), path + query);
    var content = new StringContent(body);
    {
        client.BaseAddress = endpointUri;
        content.Headers.ContentType = new MediaTypeHeaderValue("application/json");
        request.Content = content;

        var timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString();
        var data = timestamp + method + path + query + body;
        var hash = SignWithHMACSHA256(data, apiSecret);
        request.Headers.Add("ACCESS-KEY", apiKey);
        request.Headers.Add("ACCESS-TIMESTAMP", timestamp);
        request.Headers.Add("ACCESS-SIGN", hash);

        return (client, request);
        //var message = await client.SendAsync(request);
        //var response = await message.Content.ReadAsStringAsync();

        //Console.WriteLine(response);
    }
}

static string SignWithHMACSHA256(string data, string secret)
{
    using (var encoder = new HMACSHA256(Encoding.UTF8.GetBytes(secret)))
    {
        var hash = encoder.ComputeHash(Encoding.UTF8.GetBytes(data));
        return ToHexString(hash);
    }
}

static string ToHexString(byte[] bytes)
{
    var sb = new StringBuilder(bytes.Length * 2);
    foreach (var b in bytes)
    {
        sb.Append(b.ToString("x2"));
    }
    return sb.ToString();
}

static MediaTypeHeaderValue _mediaType = new MediaTypeHeaderValue("application/json");
[Benchmark]
public async Task<(HttpClient, HttpRequestMessage)> Update()
{
    var method = HttpMethod.Post;
    var path = "/v1/me/sendchildorder";
    var query = "";
    var body = @"";

    //using (var client = new HttpClient())
    //using (var request = new HttpRequestMessage(new HttpMethod(method), path + query))
    //using (var content = new StringContent(body))
    var client = new HttpClient();
    var request = new HttpRequestMessage(method, path + query);
    var content = new StringContent(body);
    {
        client.BaseAddress = endpointUri;
        content.Headers.ContentType = _mediaType;
        request.Content = content;

        var timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString();
        var data = timestamp + method + path + query + body;
        var hash = SignWithHMACSHA256New(data);
        request.Headers.Add("ACCESS-KEY", apiKey);
        request.Headers.Add("ACCESS-TIMESTAMP", timestamp);
        request.Headers.Add("ACCESS-SIGN", hash);

        return (client, request);
        //var message = await client.SendAsync(request);
        //var response = await message.Content.ReadAsStringAsync();

        //Console.WriteLine(response);
    }
}
static HMACSHA256 _hmac = new HMACSHA256(Encoding.UTF8.GetBytes(apiSecret));

static string SignWithHMACSHA256New(string data)
{
    byte[] hash;
    lock (_hmac)
    {
        hash = _hmac.ComputeHash(Encoding.UTF8.GetBytes(data));
    }
    return ToHex_Lower(hash);
}

public static string ToHex_Lower(ReadOnlySpan<byte> bytes)
{
    const string HexValues = "0123456789abcdef";

    Span<char> chars = stackalloc char[bytes.Length * 2];

    for (int i = 0; i < bytes.Length; i++)
    {
        var b = bytes[i];
        chars[i * 2] = HexValues[b >> 4];
        chars[i * 2 + 1] = HexValues[b & 0xF];
    }

    return new string(chars);
}

usingを外しているのは、Orderの場合、Disposeの部分は計測外にしたいと思って外しています。

コメントする

メールアドレスが公開されることはありません。 が付いている欄は必須項目です