原文地址:for-loop 与 json.Unmarshal 性能分析概要

前言

在项目中,常常会遇到循环交换赋值的数据处理场景,尤其是 RPC,数据交互格式要转为 Protobuf,赋值是无法避免的。一般会有如下几种做法:

  • for
  • for range
  • json.Marshal/Unmarshal

这时候又面临 “选择困难症”,用哪个好?又想代码量少,又担心性能有没有影响啊...

为了弄清楚这个疑惑,接下来将分别编写三种使用场景。来简单看看它们的性能情况,看看谁更 “好”

功能代码

...
type Person struct {
Name string `json:"name"`
Age int `json:"age"`
Avatar string `json:"avatar"`
Type string `json:"type"`
} type AgainPerson struct {
Name string `json:"name"`
Age int `json:"age"`
Avatar string `json:"avatar"`
Type string `json:"type"`
} const MAX = 10000 func InitPerson() []Person {
var persons []Person
for i := 0; i < MAX; i++ {
persons = append(persons, Person{
Name: "EDDYCJY",
Age: i,
Avatar: "https://github.com/EDDYCJY",
Type: "Person",
})
} return persons
} func ForStruct(p []Person, count int) {
for i := 0; i < count; i++ {
_, _ = i, p[i]
}
} func ForRangeStruct(p []Person) {
for i, v := range p {
_, _ = i, v
}
} func JsonToStruct(data []byte, againPerson []AgainPerson) ([]AgainPerson, error) {
err := json.Unmarshal(data, &againPerson)
return againPerson, err
} func JsonIteratorToStruct(data []byte, againPerson []AgainPerson) ([]AgainPerson, error) {
var jsonIter = jsoniter.ConfigCompatibleWithStandardLibrary
err := jsonIter.Unmarshal(data, &againPerson)
return againPerson, err
}

测试代码

...
func BenchmarkForStruct(b *testing.B) {
person := InitPerson()
count := len(person)
b.ResetTimer()
for i := 0; i < b.N; i++ {
ForStruct(person, count)
}
} func BenchmarkForRangeStruct(b *testing.B) {
person := InitPerson() b.ResetTimer()
for i := 0; i < b.N; i++ {
ForRangeStruct(person)
}
} func BenchmarkJsonToStruct(b *testing.B) {
var (
person = InitPerson()
againPersons []AgainPerson
)
data, err := json.Marshal(person)
if err != nil {
b.Fatalf("json.Marshal err: %v", err)
} b.ResetTimer()
for i := 0; i < b.N; i++ {
JsonToStruct(data, againPersons)
}
} func BenchmarkJsonIteratorToStruct(b *testing.B) {
var (
person = InitPerson()
againPersons []AgainPerson
)
data, err := json.Marshal(person)
if err != nil {
b.Fatalf("json.Marshal err: %v", err)
} b.ResetTimer()
for i := 0; i < b.N; i++ {
JsonIteratorToStruct(data, againPersons)
}
}

测试结果

BenchmarkForStruct-4                    500000          3289 ns/op           0 B/op           0 allocs/op
BenchmarkForRangeStruct-4 200000 9178 ns/op 0 B/op 0 allocs/op
BenchmarkJsonToStruct-4 100 19173117 ns/op 2618509 B/op 40036 allocs/op
BenchmarkJsonIteratorToStruct-4 300 4116491 ns/op 3694017 B/op 30047 allocs/op

从测试结果来看,性能排名为:for < for range < json-iterator < encoding/json。接下来我们看看是什么原因导致了这样子的排名?

性能对比

for-loop

在测试结果中,for range 在性能上相较 for 差。这是为什么呢?在这里我们可以参见 for range实现,伪实现如下:

for_temp := range
len_temp := len(for_temp)
for index_temp = 0; index_temp < len_temp; index_temp++ {
value_temp = for_temp[index_temp]
index = index_temp
value = value_temp
original body
}

通过分析伪实现,可得知 for range 相较 for 多做了如下事项

Expression

RangeClause = [ ExpressionList "=" | IdentifierList ":=" ] "range" Expression .

在循环开始之前会对范围表达式进行求值,多做了 “解” 表达式的动作,得到了最终的范围值

Copy

...
value_temp = for_temp[index_temp]
index = index_temp
value = value_temp
...

从伪实现上可以得出,for range 始终使用值拷贝的方式来生成循环变量。通俗来讲,就是在每次循环时,都会对循环变量重新分配

小结

通过上述的分析,可得知其比 for 慢的原因是 for range 有额外的性能开销,主要为值拷贝的动作导致的性能下降。这是它慢的原因

那么其实在 for range 中,我们可以使用 _T[i] 也能达到和 for 差不多的性能。但这可能不是 for range 的设计本意了

json.Marshal/Unmarshal

encoding/json

json 互转是在三种方案中最慢的,这是为什么呢?

众所皆知,官方的 encoding/json 标准库,是通过大量反射来实现的。那么 “慢”,也是必然的。可参见下述代码:

...
func newTypeEncoder(t reflect.Type, allowAddr bool) encoderFunc {
...
switch t.Kind() {
case reflect.Bool:
return boolEncoder
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return intEncoder
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
return uintEncoder
case reflect.Float32:
return float32Encoder
case reflect.Float64:
return float64Encoder
case reflect.String:
return stringEncoder
case reflect.Interface:
return interfaceEncoder
case reflect.Struct:
return newStructEncoder(t)
case reflect.Map:
return newMapEncoder(t)
case reflect.Slice:
return newSliceEncoder(t)
case reflect.Array:
return newArrayEncoder(t)
case reflect.Ptr:
return newPtrEncoder(t)
default:
return unsupportedTypeEncoder
}
}

既然官方的标准库存在一定的 “问题”,那么有没有其他解决方法呢?目前在社区里,大多为两类方案。如下:

  • 预编译生成代码(提前确定类型),可以解决运行时的反射带来的性能开销。缺点是增加了预生成的步骤
  • 优化序列化的逻辑,性能达到最大化

接下来的实验,我们用第二种方案的库来测试,看看有没有改变。另外也推荐大家了解如下项目:

json-iterator/go

目前社区较常用的是 json-iterator/go,我们在测试代码中用到了它

它的用法与标准库 100% 兼容,并且性能有较大提升。我们一起粗略的看下是怎么做到的,如下:

reflect2

利用 modern-go/reflect2 减少运行时调度开销

...
type StructDescriptor struct {
Type reflect2.Type
Fields []*Binding
} ...
type Binding struct {
levels []int
Field reflect2.StructField
FromNames []string
ToNames []string
Encoder ValEncoder
Decoder ValDecoder
} type Extension interface {
UpdateStructDescriptor(structDescriptor *StructDescriptor)
CreateMapKeyDecoder(typ reflect2.Type) ValDecoder
CreateMapKeyEncoder(typ reflect2.Type) ValEncoder
CreateDecoder(typ reflect2.Type) ValDecoder
CreateEncoder(typ reflect2.Type) ValEncoder
DecorateDecoder(typ reflect2.Type, decoder ValDecoder) ValDecoder
DecorateEncoder(typ reflect2.Type, encoder ValEncoder) ValEncoder
}
struct Encoder/Decoder Cache

类型为 struct 时,只需要反射一次 Name 和 Type,会缓存 struct Encoder 和 Decoder

var typeDecoders = map[string]ValDecoder{}
var fieldDecoders = map[string]ValDecoder{}
var typeEncoders = map[string]ValEncoder{}
var fieldEncoders = map[string]ValEncoder{}
var extensions = []Extension{} .... fieldNames := calcFieldNames(field.Name(), tagParts[0], tag)
fieldCacheKey := fmt.Sprintf("%s/%s", typ.String(), field.Name())
decoder := fieldDecoders[fieldCacheKey]
if decoder == nil {
decoder = decoderOfType(ctx.append(field.Name()), field.Type())
}
encoder := fieldEncoders[fieldCacheKey]
if encoder == nil {
encoder = encoderOfType(ctx.append(field.Name()), field.Type())
}
文本解析优化

小结

相较于官方标准库,第三方库 json-iterator/go 在运行时上做的更好。这是它快的原因

有个需要注意的点,在 Go1.10 后 map 类型与标准库的已经没有太大的性能差异。但是,例如 struct 类型等仍然有较大的性能提高

总结

在本文中,我们首先进行了性能测试,再分析了不同方案,得知为什么了快慢的原因。那么最终在选择方案时,可以根据不同的应用场景去抉择:

  • 对性能开销有较高要求:选用 for,开销最小
  • 中规中矩:选用 for range,大对象慎用
  • 量小、占用小、数量可控:选用 json.Marshal/Unmarshal 的方案也可以。其重复代码少,但开销最大

在绝大多数场景中,使用哪种并没有太大的影响。但作为工程师你应当清楚其利弊。以上就是不同的方案分析概要,希望对你有所帮助 :)

for-loop 与 json.Unmarshal 性能分析概要的更多相关文章

  1. MySQL 索引性能分析概要

    上一篇文章 MySQL 索引设计概要 介绍了影响索引设计的几大因素,包括过滤因子.索引片的宽窄与大小以及匹配列和过滤列.在文章的后半部分介绍了 数据库索引设计与优化 一书中,理想的三星索引的设计流程和 ...

  2. 三 概要模式 2) MR倒排索引、性能分析、搜索干扰词。

    二  倒排索引     倒排索引(英语:Inverted index),也常被称为反向索引.置入档案或反向档案,是一种索引方法,被用来存储在全文搜索下某个单词在一个文档或者一组文档中的存储位置的映射. ...

  3. jstack(查看线程)、jmap(查看内存)和jstat(性能分析)

    公司内部同事分享的一篇文章 周末看到一个用jstack查看死锁的例子.昨天晚上总结了一下jstack(查看线程).jmap(查看内存)和jstat(性能分析)命令.供大家参考 1.Jstack 1.1 ...

  4. jstack(查看线程)、jmap(查看内存)和jstat(性能分析)命令

    jstack(查看线程).jmap(查看内存)和jstat(性能分析)命令   公司内部同事分享的一篇文章 周末看到一个用jstack查看死锁的例子.昨天晚上总结了一下jstack(查看线程).jma ...

  5. 命令:jstack(查看线程)、jmap(查看内存)和jstat(性能分析)命令

    命令:jstack(查看线程).jmap(查看内存)和jstat(性能分析)命令 这些命令 必须 在 linux jdk bin 路径 下执行 eq: ./jstack 10303 即可  如果想把 ...

  6. Java 性能分析工具 , 第 1 部分: 操作系统工具

    引言 性能分析的前提是将应用程序内部的运行状况以及应用运行环境的状况以一种可视化的方式更加直接的展现出来,如何来达到这种可视化的展示呢?我们需要配合使用操作系统中集成的程序监控工具和 Java 中内置 ...

  7. Oracle Update 语句语法与性能分析 - 多表关联

    Oracle Update 语句语法与性能分析 - 多表关联   为了方便起见,建立了以下简单模型,和构造了部分测试数据: 在某个业务受理子系统BSS中, SQL 代码 --客户资料表 create ...

  8. Java 集合系列08之 List总结(LinkedList, ArrayList等使用场景和性能分析)

    概要 前面,我们学完了List的全部内容(ArrayList, LinkedList, Vector, Stack). Java 集合系列03之 ArrayList详细介绍(源码解析)和使用示例 Ja ...

  9. 性能分析之-- JAVA Thread Dump 分析综述

    性能分析之-- JAVA Thread Dump 分析综述       一.Thread Dump介绍 1.1什么是Thread Dump? Thread Dump是非常有用的诊断Java应用问题的工 ...

随机推荐

  1. 1012 The Best Rank (25 分)

    To evaluate the performance of our first year CS majored students, we consider their grades of three ...

  2. PTA数据结构与算法题目集(中文) 7-8

    PTA数据结构与算法题目集(中文)  7-8 7-8 哈利·波特的考试 (25 分)   哈利·波特要考试了,他需要你的帮助.这门课学的是用魔咒将一种动物变成另一种动物的本事.例如将猫变成老鼠的魔咒是 ...

  3. Spring Taco Cloud——配料表(Lombok的安装及其注解)

    先简单概括下这个项目:外卖订餐平台Web,这个是承接我的这篇博文来续写的https://www.cnblogs.com/yycjavastudy/articles/12666104.html,在这一篇 ...

  4. 【WPF学习】第六十六章 支持可视化状态

    上一章介绍的ColorPicker控件,是控件设计的最好示例.因为其行为和可视化外观是精心分离的,所以其他设计人员可开发动态改变其外观的新模板. ColorPicker控件如此简单的一个原因是不涉及状 ...

  5. hadoop(学习)

                                                                                hadoop Hadoop是一个用于海量数据统计 ...

  6. 【网络编程01】socket的基础知识-简单网络通信程序

    1.什么是socket socket(套接字),简单来说是IP地址与端口(port)的组合,可以与远程主机的应用程序进行通信.通过IP地址可以确定一台主机,而通过端口则可以确定某一个应用程序.IP+端 ...

  7. Struts2-学习笔记系列(10)-自定义类型转换

    注意name=user和对应action中的实例名称一致 这些代码是写在HTML文件中的 <s:form action="login"> <s:textfield ...

  8. 区间dp入门+例题

    区间dp作为线性dp的一种,顾名思义是以区间作为阶段进行dp的,使用它的左右端点描述每个维度,决策往往是从小状态向大状态转移中推得的.它跟st表等树状结构有着相似的原理---向下划分,向上递推. dp ...

  9. 七、环回接口ip地址(逻辑接口)

    loopback接口,在网络设备(一般是路由器)上是一种特殊的接口,它不是物理接口,而是一种看不见摸不着的逻辑接口(也称虚拟接口),但是对于网络设备来说却是至关重要的. 在网络设备上可以通过配置命令来 ...

  10. 关于C++线程池的实现的思考

    今天突然对前些日子一直很疑惑的c++线程池有了新的想法.其实所谓的线程池无非就是两个技术点,一个,多线程,指工作线程和主线程分离,或者说数据接收和处理分两个线程,一般就是讲需要运行的函数放到子线程执行 ...