以下の内容は.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の部分は計測外にしたいと思って外しています。