Put
Some[Types]
on your
HTTP endpoints
型を気に掛けてくれるのは TCP や HTTP ではなくコンパイラ
あなたも気に掛けるべき
HttpRequest
, HttpResponse
?HttpEntity
, HttpHeader
, HttpParam
, ...?String
?[A]
Endpoint[A]
Finch は Finagle 等を使って開発した高速な HTTP コンビネータ
20 以上のビジネスで本番運用されている
map
::
and coproduct :+:
Input
and produces Future[Output[A]]
Endpoint は functor と applicative であり、中に state を持つ
new Service[Request, Response] {
def apply(req: Request): Future[Response] =
if (req.method != Method.Post)
Future.value(Response(req.version, Status.NotFound))
else {
val payloadIn =
mapper.readValue(req.contentString, classOf[Payload])
val payloadOut =
mapper.writeValueAsBytes(payloadIn)
val rep = Response(req.version, Status.Ok)
rep.content = Buf.ByteArray.Owned(payloadOut)
rep.contentType = "application/json"
Future.value(rep)
}
}
post(jsonBody[Payload])
Finagle と Finch の比較
Finagle に対する Finch のオーバヘッド
たった 10%? コンパイル時の処理は非常に軽い
10% も? 合成はメモリ確保のコストがかかる
高速な Scala のコードは Scala っぽくない
My new pet peeve: "how to make X faster: do less of X" recommendations.
— Gil Tene (@giltene) January 9, 2017
「X をより早くするには X を減らせ」
例 1: 合成を早くするには? 合成を減らせ
type EndpointResult[A] = Option[(Input, Future[Output[A]])]
sealed abstract class EndpointResult[+A]
case object Skipped extends EndpointResult[Nothing]
case object Matched[A](
rem: Input, out: Future[Output[A]]) extends EndpoinResult[A]
エンドポイントの結果をモデル化する
-------------------------------------------------------------------------------------
TA: Type Alias | ADT: sealed abstract class | Running Time Mode
-------------------------------------------------------------------------------------
MapBenchmark.mapAsyncTA avgt 429.113 ± 43.297 ns/op
MapBenchmark.mapAsyncADT avgt 407.126 ± 12.807 ns/op
MapBenchmark.mapOutputAsyncTA avgt 821.786 ± 52.045 ns/op
MapBenchmark.mapOutputAsyncADT avgt 777.654 ± 26.444 ns/op
MapBenchmark.mapAsyncTA:·gc.alloc.rate.norm avgt 776.000 ± 0.001 B/op
MapBenchmark.mapAsyncADT:·gc.alloc.rate.norm avgt 720.000 ± 0.001 B/op
MapBenchmark.mapOutputAsyncTA:·gc.alloc.rate.norm avgt 1376.001 ± 0.001 B/op
MapBenchmark.mapOutputAsyncADT:·gc.alloc.rate.norm avgt 1320.001 ± 0.001 B/op
See MapBenchmark and #707.
ベンチマーク結果
まとめ: 性能のためには新しい型を導入する
カリカリな局面で使う抽象データでは型合成を避ける
例 2: コーデックを早くするには? エンコード・デコードを減らせ
For UTF-8 we need at least 1 character per byte and each char
on JVM is 2 bytes.
UTF-8 の 1 文字は JVM 上では 2 バイトになる
多くの JSON ライブラリはバイト列から JSON をパースできる
Finch は 0.11 からサポート
implicit def fromCirce[A: Decoder]: Decode.Json[A] =
Decode.json { (b, cs) =>
val attemptJson = cs match {
case StandardCharsets.UTF_8 => decodeByteBuffer[A](b.asByteBuffer)
case _ => decode[A](BufText.extract(b, cs))
}
attemptJson.fold[Try[A]](Throw.apply, Return.apply)
}
Circe を使ったバイト列から JSON へのパース
---------------------------------------------------------------------------------
S: parse string | BA: parse byte array | Running Time Mode
---------------------------------------------------------------------------------
JsonBenchmark.decodeS avgt 5950.402 ± 464.246 ns/op
JsonBenchmark.decodeBA avgt 3232.696 ± 171.160 ns/op
JsonBenchmark.decodeS:·gc.alloc.rate.norm avgt 7992.005 ± 12.749 B/op
JsonBenchmark.decodeBA:·gc.alloc.rate.norm avgt 4908.003 ± 6.374 B/op
See JsonBenchmark.
デコードのベンチマーク
Each UTF-8 character on JVM produces up to 3 bytes. See UTF_8.newEncoder.maxBytesPerChar
JVM 上の UTF-8 文字をエンコードすると最大で 3 バイトになる
UTF_8.newEncoder.maxBytesPerChar を参照
implicit def fromCirce[A](implicit e: Encoder[A]): Encode.Json[A] =
Encode.json {
case (a, StandardCharsets.UTF_8) =>
Buf.ByteBuffer.Owned(printBytes(e(a)))
case (a, cs) =>
BufText(printString(e(a)), cs)
}
Circe を使った JSON のバイト列出力
---------------------------------------------------------------------------------
S: print string | BA: print byte array | Running Time Mode
---------------------------------------------------------------------------------
JsonBenchmark.encodeS avgt 16400.327 ± 621.935 ns/op
JsonBenchmark.encodeBA avgt 12645.070 ± 391.591 ns/op
JsonBenchmark.encodeS:·gc.alloc.rate.norm avgt 46900.015 ± 19.123 B/op
JsonBenchmark.encodeBA:·gc.alloc.rate.norm avgt 30360.011 ± 0.001 B/op
See JsonBenchmark.
エンコードのベンチマーク
new String(bytes, "UTF-8")
allocates 2 * bytes.length bytesString.getBytes("UTF-8")
allocates 3 * string.size bytesまとめ: マイクロな最適化も良いけど、問題を端から端まで見て何を減らせるか考えよう
Finch 0.12 の文字列レスのコーデックを試してみてください!
性能を推測せずに、JMH で計測する
メモリ確保を減らす方法を学ぶ