English | 中文
A very fast dynamic Thrift serializer & deserializer without generating code.
It implements a pure Go version (or reflect version) and a just-in-time(JIT) compilation version. Since the reflect version performs better than the JIT version in most cases and it works on different cpu architectures, so we plan to deprecate the JIT version starting with Go 1.24.
Before Go 1.24:
- JIT version is the default serializer & deserializer for
amd64
. - reflect version is only enabled when running on
!amd64
orwindows
- You can enable the reflect version by specifying os env
FRUGAL_NO_JIT=1
Since Go 1.24:
- reflect version is the default serializer & deserializer.
- JIT code will be skipped by go build tags.
Traditional Thrift serializer and deserializer are based on generated code which is no longer needed since we can make use of struct field tags.
Based on the test cases in frugal/tests
, Frugal's performance is 1 to 4 times better than Apache Thrift (TBinaryProtocol).
There may be variations between different test cases. Feel free to share your test cases with us.
go version go1.22.5 linux/amd64
goos: linux
goarch: amd64
pkg: github.com/cloudwego/frugal/tests
cpu: Intel(R) Xeon(R) Gold 5118 CPU @ 2.30GHz
Marshal_ApacheThrift/small-4 2070745 584.0 ns/op 998.32 MB/s 112 B/op 1 allocs/op
Marshal_ApacheThrift/medium-4 78729 13680 ns/op 1280.57 MB/s 112 B/op 1 allocs/op
Marshal_ApacheThrift/large-4 3097 376184 ns/op 1179.75 MB/s 620 B/op 1 allocs/op
Marshal_Frugal_JIT/small-4 4939591 242.1 ns/op 2407.83 MB/s 13 B/op 0 allocs/op
Marshal_Frugal_JIT/medium-4 160820 7485 ns/op 2340.29 MB/s 54 B/op 0 allocs/op
Marshal_Frugal_JIT/large-4 5370 214258 ns/op 2071.35 MB/s 338 B/op 0 allocs/op
Marshal_Frugal_Reflect/small-4 10171197 117.3 ns/op 4970.90 MB/s 0 B/op 0 allocs/op
Marshal_Frugal_Reflect/medium-4 180207 6644 ns/op 2636.73 MB/s 0 B/op 0 allocs/op
Marshal_Frugal_Reflect/large-4 6312 185534 ns/op 2392.04 MB/s 0 B/op 0 allocs/op
Unmarshal_ApacheThrift/small-4 768525 1443 ns/op 403.94 MB/s 1232 B/op 5 allocs/op
Unmarshal_ApacheThrift/medium-4 24463 47067 ns/op 372.19 MB/s 44816 B/op 176 allocs/op
Unmarshal_ApacheThrift/large-4 1053 1155725 ns/op 384.00 MB/s 1135540 B/op 4433 allocs/op
Unmarshal_Frugal_JIT/small-4 2575767 466.3 ns/op 1250.36 MB/s 547 B/op 2 allocs/op
Unmarshal_Frugal_JIT/medium-4 62128 19333 ns/op 906.12 MB/s 19404 B/op 89 allocs/op
Unmarshal_Frugal_JIT/large-4 2328 496431 ns/op 893.99 MB/s 495906 B/op 2283 allocs/op
Unmarshal_Frugal_Reflect/small-4 2770252 437.2 ns/op 1333.60 MB/s 544 B/op 1 allocs/op
Unmarshal_Frugal_Reflect/medium-4 64232 18183 ns/op 963.45 MB/s 19945 B/op 57 allocs/op
Unmarshal_Frugal_Reflect/large-4 2325 496415 ns/op 894.02 MB/s 511876 B/op 1467 allocs/op
Use Frugal as Kitex serializer and deserializer
No more massive serialization and deserialization code, leads to a more tidy project. No more meaningless diff of generated code in code review.
Serialized and Deserialize struct generated by Thriftgo
If you have a Thrift file, and all you need is using Frugal to do serialization and deserialization. You can use thriftgo to generate Go struct, then you can use Frugal.
If you don't want any Thrift files, and you want serialize or deserialize a customized Go struct. You can add some struct field tag to the Go struct, then you can use Frugal.
go get github.com/cloudwego/kitex@latest
Example:
kitex -thrift frugal_tag -service a.b.c my.thrift
If you don't need codec code, you can use -thrift template=slim
option.
kitex -thrift frugal_tag,template=slim -service a.b.c my.thrift
Client example:
package client
import (
"context"
"example.com/kitex_test/client/kitex_gen/a/b/c/echo"
"github.com/cloudwego/kitex/client"
"github.com/cloudwego/kitex/pkg/remote/codec/thrift"
)
func Echo() {
code := thrift.NewThriftCodecWithConfig(thrift.FastRead | thrift.FastWrite | thrift.FrugalRead | thrift.FrugalWrite)
cli := echo.MustNewClient("a.b.c", client.WithPayloadCodec(codec))
...
}
Server example:
package main
import (
"log"
"github.com/cloudwego/kitex/server"
c "example.com/kitex_test/kitex_gen/a/b/c/echo"
"github.com/cloudwego/kitex/pkg/remote/codec/thrift"
)
func main() {
code := thrift.NewThriftCodecWithConfig(thrift.FastRead | thrift.FastWrite | thrift.FrugalRead | thrift.FrugalWrite)
svr := c.NewServer(new(EchoImpl), server.WithPayloadCodec(code))
err := svr.Run()
if err != nil {
log.Println(err.Error())
}
}
We can define a struct in Thrift file like below:
my.thrift:
struct MyStruct {
1: string msg
2: i64 code
}
Now we have thrift file, we can use Thriftgo with frugal_tag
option to generate Go code.
Example:
thriftgo -r -o thrift -g go:frugal_tag,package_prefix=example.com/kitex_test/thrift my.thrift
If you don't need codec code, you can use template=slim
option
thriftgo -r -o thrift -g go:frugal_tag,template=slim,package_prefix=example.com/kitex_test/thrift my.thrift
Now we can use Frugal to serialize or deserialize the struct defined in thrift file.
Example:
package main
import (
"github.com/cloudwego/frugal"
"example.com/kitex_test/thrift"
)
func main() {
ms := &thrift.MyStruct{
Msg: "my message",
Code: 1024,
}
...
buf := make([]byte, frugal.EncodedSize(ms))
frugal.EncodeObject(buf, nil, ms)
...
got := &thrift.MyStruct{}
frugal.DecodeObject(buf, got)
...
}
We can define a struct like this:
type MyStruct struct {
Msg string
Code int64
Numbers []int64
}
Frugal tag is like frugal:"1,default,string"
, 1
is field ID, default
is field requiredness, string
is field type. Field ID and requiredness is always required, but field type is only required for list
, set
and enum
.
You can add Frugal tag to MyStruct
like below:
type MyStruct struct {
Msg string `frugal:"1,default"`
Code int64 `frugal:"2,default"`
Numbers []int64 `frugal:"3,default,list<i64>"`
}
All types example:
type MyEnum int64
type Example struct {
MyOptBool *bool `frugal:"1,optional"`
MyReqBool bool `frugal:"2,required"`
MyOptByte *int8 `frugal:"3,optional"`
MyReqByte int8 `frugal:"4,required"`
MyOptI16 *int16 `frugal:"5,optional"`
MyReqI16 int16 `frugal:"6,required"`
MyOptI32 *int32 `frugal:"7,optional"`
MyReqI32 int32 `frugal:"8,required"`
MyOptI64 *int64 `frugal:"9,optional"`
MyReqI64 int64 `frugal:"10,required"`
MyOptString *string `frugal:"11,optional"`
MyReqString string `frugal:"12,required"`
MyOptBinary []byte `frugal:"13,optional"`
MyReqBinary []byte `frugal:"14,required"`
MyOptI64Set []int64 `frugal:"15,optional,set<i64>"`
MyReqI64Set []int64 `frugal:"16,required,set<i64>"`
MyOptI64List []int64 `frugal:"17,optional,list<i64>"`
MyReqI64List []int64 `frugal:"18,required,list<i64>"`
MyOptI64StringMap map[int64]string `frugal:"19,optional"`
MyReqI64StringMap map[int64]string `frugal:"20,required"`
MyOptEnum *MyEnum `frugal:"21,optional,i64"`
MyReqEnum *MyEnum `frugal:"22,optional,i64"`
}
Example:
package main
import (
"github.com/cloudwego/frugal"
)
func main() {
ms := &thrift.MyStruct{
Msg: "my message",
Code: 1024,
Numbers: []int64{0, 1, 2, 3, 4},
}
...
buf := make([]byte, frugal.EncodedSize(ms))
frugal.EncodeObject(buf, nil, ms)
...
got := &thrift.MyStruct{}
frugal.DecodeObject(buf, got)
...
}