51Testing软件测试论坛

 找回密码
 (注-册)加入51Testing

QQ登录

只需一步,快速开始

微信登录,快人一步

查看: 954|回复: 0
打印 上一主题 下一主题

Go 泛型基准测试:性能更差还是更好?

[复制链接]
  • TA的每日心情
    擦汗
    昨天 09:04
  • 签到天数: 942 天

    连续签到: 1 天

    [LV.10]测试总司令

    跳转到指定楼层
    1#
    发表于 2022-4-22 10:26:14 | 只看该作者 回帖奖励 |倒序浏览 |阅读模式
    Go1.18 已经发布了,泛型终于正式进入了 Go 语言。那泛型将如何影响性能?让我们通过对几个用例进行基准测试来弄清楚。
      关于 Go1.18 新特性的文章有很多,讨论也不少。其中一个讨论是我想写的一个主题,即泛型对性能有什么影响?许多读者担心泛型会降低性能,但我的观点是泛型会提高性能。我的观点背后的原因是泛型将允许我们在运行时跳过类型转换、断言和反射,而是依赖编译器在编译时决定这个问题。
      在我关于学习泛型[1]的文章中,我解释了泛型的用法,两个主要好处是减少了基于数据类型的重复函数并避免了interface{}. 这些是我们将在本文中进行基准测试的用例,以发现更改的性能。
      说明下:我不是基准测试专家。我只是一个基准测试菜鸟。在我看来,基准测试非常困难。
      为了做出公平的基准测试,我们将为每个用例设置一个测试用例。这将意味着我们将:
      ·使用重复函数进行基准测试
      · 使用泛型进行基准测试
      · 使用使用 interface{} 进行基准测试
      准备函数进行基准测
      试我们将重用学习泛型[2]中的一些代码,在其中,我们有一个Subtract函数可以减去三种Subtractable数据类型之间的值。
      我们将要确定哪些 Subtract 方法性能最好。可以在 Playground[3] 尝试一下。
    1. package functions
    2.   // Subtract will subtract the second value from the first
    3.   func SubtractInt(a, b int) int {
    4.    return a - b
    5.   }
    6.   // Subtract64 will subtract the second value from the first
    7.   func SubtractInt64(a, b int) int {
    8.    return a - b
    9.   }
    10.   // SubtractFloat32 will subtract the second value from the first
    11.   func SubtractFloat32(a, b float32) float32 {
    12.    return a - b
    13.   }
    14.   // SubtractTypeSwitch is used to subtract using interfaces
    15.   func SubtractTypeSwitch(a, b interface{}) interface{} {
    16.    switch a.(type) {
    17.    case int:
    18.     return a.(int) - b.(int)
    19.    case int64:
    20.     return a.(int64) - b.(int64)
    21.    case float32:
    22.     return a.(float32) - b.(float32)
    23.    default:
    24.     return nil
    25.    }
    26.   }
    27.   // Subtract will subtract the second value from the first
    28.   func Subtract[V int64 | int | float32](a, b V "V int64 | int | float32") V {
    29.    return a - b
    30.   }
    复制代码
    在那里,我们将开始对功能进行基准测试。它们应该相当容易理解,并且我们涵盖了减法、基于数据类型、类型切换和泛型的可能解决方案。
      准备基准测试
      创建一个常规的测试文件,我们可以在其中存储基准,如果你熟悉 Go 中的基准,你可以阅读这里的教程[4]。
      在基准测试的顶部,我将生成两个切片,一个随机整数切片,一个随机 float32 切片。这些随机切片将用作减法方法的输入参数。
      然后我们创建一个b.Run函数,它会一次触发一个函数,次数与我们设置为基准测试器的次数一样多,使用-benchtime标志运行。对于这个基准测试,我将强制基准测试器运行每个函数 1000000000 次。如果你未指定运行函数的次数,则基准测试程序会在特定时间内尽可能多次地运行该函数。这将以它们没有运行相同数量的操作而告终,我希望它们这样做。
      这就是我最终的基准测试的样子。
      用于执行基准测试以确定泛型性能影响的测试文件。
    1. package functions
    2.   import (
    3.    "math/rand"
    4.    "testing"
    5.    "time"
    6.   )
    7.   // Benchmark_Subtract is used to determine the most performant solution to subtraction
    8.   func Benchmark_Subtract(b *testing.B) {
    9.    // Create a slice of random numbers based on the number of iterations set
    10.    // to test the performance of the function
    11.    // Default iterations for me is 1000000000
    12.    // b.N is always 1 so we can use that to set the number of iterations
    13.    numbers := make([]int, 1000000001)
    14.    floatNumbers := make([]float32, 1000000001)
    15.    // Create a random seed
    16.    seed := rand.NewSource(time.Now().UnixNano())
    17.    // Give the seed to the random package
    18.    randomizer := rand.New(seed)
    19.    for i := 0; i < b.N; i++ {
    20.     // randomize numbers between 0-100
    21.     numbers[i] = randomizer.Intn(100)
    22.     floatNumbers[i] = float32(randomizer.Intn(100))
    23.    }
    24.    // run a benchmark for regular Ints
    25.    b.Run("SubtractInt", func(b *testing.B) {
    26.     for i := 0; i < b.N; i++ {
    27.      SubtractInt(numbers[i], numbers[i+1])
    28.     }
    29.    })
    30.    // run a benchmark for regular Floats
    31.    b.Run("SubtractFloat", func(b *testing.B) {
    32.     for i := 0; i < b.N; i++ {
    33.      SubtractFloat32(floatNumbers[i], floatNumbers[i+1])
    34.     }
    35.    })
    36.    // run a benchmark for TypeSwitched Ints
    37.    b.Run("Type_Subtraction_int", func(b *testing.B) {
    38.     for i := 0; i < b.N; i++ {
    39.      SubtractTypeSwitch(numbers[i], numbers[i+1])
    40.     }
    41.    })
    42.    // run a benchmark for TypeSwitched Floats
    43.    b.Run("Type_Subtraction_float", func(b *testing.B) {
    44.     for i := 0; i < b.N; i++ {
    45.      SubtractTypeSwitch(floatNumbers[i], floatNumbers[i+1])
    46.     }
    47.    })
    48.    // run a benchmark for Generic Ints
    49.    b.Run("Generic_Subtraction_int", func(b *testing.B) {
    50.     for i := 0; i < b.N; i++ {
    51.      Subtract[int](numbers[i], numbers[i+1] "int")
    52.     }
    53.    })
    54.    // run a benchmark for Generic Floats
    55.    b.Run("Generic_Subtraction_float", func(b *testing.B) {
    56.     for i := 0; i < b.N; i++ {
    57.      Subtract[float32](floatNumbers[i], floatNumbers[i+1] "float32")
    58.     }
    59.    })
    60.    // run a benchmark where generic type is infered
    61.    b.Run("Generic_Inferred_int", func(b *testing.B) {
    62.     for i := 0; i < b.N; i++ {
    63.      Subtract(numbers[i], numbers[i+1])
    64.     }
    65.    })
    66.   }
    复制代码
    在泛型基准测试中,基准测试将测试所有用例中int和float32的减法函数,我添加了第三个选项,推断数据类型。我还想确定如果我们让泛型函数将数据类型推断为int会有怎样的表现。
      要运行基准测试,请使用以下命令。请注意,该-count 5参数用于将每个基准测试运行 5 次。这是因为如果你运行每个基准测试一次,你可能会得到不公平的结果。
    1.   go test -v -bench=Benchmark -benchtime=1000000000x -count 5
    复制代码
    分析结果
      基准测试将与正在运行的函数的名称一起输出,我们可以使用它来识别不同的函数。第二个值是运行的操作数,在我们的例子中,我们将其设置为固定数字,因此所有行都应该显示相同。
      第三个输出很有趣,它是每次操作的纳秒数 (ns/op)。这是显示函数平均速度的指标。
      Go 测试工具的基准测试结果。
    1.  goos: windows
    2.   goarch: amd64
    3.   pkg: programmingpercy/benchgeneric
    4.   cpu: Intel(R) Core(TM) i7-10750H CPU @ 2.60GHz
    5.   Benchmark_Subtract
    6.   Benchmark_Subtract/SubtractInt
    7.   Benchmark_Subtract/SubtractInt-4                1000000000               0.9002 ns/op
    8.   Benchmark_Subtract/SubtractInt-4                1000000000               0.8904 ns/op
    9.   Benchmark_Subtract/SubtractInt-4                1000000000               0.8277 ns/op
    10.   Benchmark_Subtract/SubtractInt-4                1000000000               0.8290 ns/op
    11.   Benchmark_Subtract/SubtractInt-4                1000000000               0.8266 ns/op
    12.   Benchmark_Subtract/SubtractFloat
    13.   Benchmark_Subtract/SubtractFloat-4              1000000000               0.8591 ns/op
    14.   Benchmark_Subtract/SubtractFloat-4              1000000000               0.8033 ns/op
    15.   Benchmark_Subtract/SubtractFloat-4              1000000000               0.8108 ns/op
    16.   Benchmark_Subtract/SubtractFloat-4              1000000000               0.8168 ns/op
    17.   Benchmark_Subtract/SubtractFloat-4              1000000000               0.8040 ns/op
    18.   Benchmark_Subtract/Type_Subtraction_int
    19.   Benchmark_Subtract/Type_Subtraction_int-4               1000000000               1.597 ns/op
    20.   Benchmark_Subtract/Type_Subtraction_int-4               1000000000               1.711 ns/op
    21.   Benchmark_Subtract/Type_Subtraction_int-4               1000000000               1.607 ns/op
    22.   Benchmark_Subtract/Type_Subtraction_int-4               1000000000               1.570 ns/op
    23.   Benchmark_Subtract/Type_Subtraction_int-4               1000000000               1.588 ns/op
    24.   Benchmark_Subtract/Type_Subtraction_float
    25.   Benchmark_Subtract/Type_Subtraction_float-4             1000000000               1.320 ns/op
    26.   Benchmark_Subtract/Type_Subtraction_float-4             1000000000               1.311 ns/op
    27.   Benchmark_Subtract/Type_Subtraction_float-4             1000000000               1.323 ns/op
    28.   Benchmark_Subtract/Type_Subtraction_float-4             1000000000               1.424 ns/op
    29.   Benchmark_Subtract/Type_Subtraction_float-4             1000000000               1.321 ns/op
    30.   Benchmark_Subtract/Generic_Subtraction_int
    31.   Benchmark_Subtract/Generic_Subtraction_int-4            1000000000               0.8251 ns/op
    32.   Benchmark_Subtract/Generic_Subtraction_int-4            1000000000               0.8288 ns/op
    33.   Benchmark_Subtract/Generic_Subtraction_int-4            1000000000               0.8420 ns/op
    34.   Benchmark_Subtract/Generic_Subtraction_int-4            1000000000               0.8377 ns/op
    35.   Benchmark_Subtract/Generic_Subtraction_int-4            1000000000               0.8357 ns/op
    36.   Benchmark_Subtract/Generic_Subtraction_float
    37.   Benchmark_Subtract/Generic_Subtraction_float-4          1000000000               0.7952 ns/op
    38.   Benchmark_Subtract/Generic_Subtraction_float-4          1000000000               0.7987 ns/op
    39.   Benchmark_Subtract/Generic_Subtraction_float-4          1000000000               0.7877 ns/op
    40.   Benchmark_Subtract/Generic_Subtraction_float-4          1000000000               0.8037 ns/op
    41.   Benchmark_Subtract/Generic_Subtraction_float-4          1000000000               0.8283 ns/op
    42.   Benchmark_Subtract/Generic_Inferred_int
    43.   Benchmark_Subtract/Generic_Inferred_int-4               1000000000               0.8297 ns/op
    44.   Benchmark_Subtract/Generic_Inferred_int-4               1000000000               0.8283 ns/op
    45.   Benchmark_Subtract/Generic_Inferred_int-4               1000000000               0.8319 ns/op
    46.   Benchmark_Subtract/Generic_Inferred_int-4               1000000000               0.8366 ns/op
    47.   Benchmark_Subtract/Generic_Inferred_int-4               1000000000               0.8623 ns/op
    48.   PASS
    49.   ok      programmingpercy/benchgeneric   37.114s
    复制代码
    从结果中,我们可以确定类型断言函数要慢得多。它*慢了大约 50-90%*。在这个测试用例中,这似乎很荒谬,因为我们谈论的是半纳秒。
      泛型函数的执行与特定于数据类型的函数大致相同,但速度略有提高。速度的这种小幅提高可能是由于我计算机上运行的其他软件。以我的心态,我认为编译器完成其工作后,泛型函数调用应该与常规函数调用相同。
      我们可以在结果中看到的另一个要点是int减法比float32减法更耗时。常规int减法的平均速度为 0,85478 ns/op,常规float32减法的平均速度为0,8188 ns/op。这意味着在我的基准测试中,float32减法大约快 5% 。
      因此,该基准的关键要点是:
      ·根据我的观点,类型断言/类型转换解决方案最慢
      · 泛型和常规数据类型函数的性能相同
      · Float32减法比int快
      以真实场景为基准
      让我们比较一个真实的场景。在用例中,我们有两个有 Move 的结构Person,Car。这两个结构都有一个Move接受距离的函数,但是,Person 距离被传递为float32 而 Car 接受一个int。
      这两种结构都在同一个工作流中处理,因此我们希望在同一个函数中处理它们。
      对此的泛型解决方案是创建泛型结构,我们可以在其中定义要在创建时使用的数据类型。接口解决方案是接受结构作为输入,并对它们进行类型断言并转换正确的数据类型。我们不能为它们提供共享接口,因为数据类型不一样。
      在代码示例中,有一个泛型和旧类型断言解决方案的实现,类型断言带有后缀Regular,因此我们可以更容易地知道什么与什么解决方案相关。
      在具有不同数据类型的Cars和Persons 上执行 Move 的泛型解决方案。
    1. package benchmarking
    2.   // Subtractable is a type constraint that defines subtractable datatypes to be used in generic functions
    3.   type Subtractable interface {
    4.    int | int64 | float32
    5.   }
    6.   // Moveable is the interace for moving a Entity
    7.   type Moveable[S Subtractable] interface {
    8.    Move(S)
    9.   }
    10.   // Car is a Generic Struct with the type S to be defined
    11.   type Car[S Subtractable] struct {
    12.    Name string
    13.    DistanceMoved S
    14.   }
    15.   // Person is a Generic Struct with the type S to be defined
    16.   type Person[S Subtractable] struct {
    17.    Name string
    18.    DistanceMoved S
    19.   }
    20.   // Person is a struct that accepts a type definition at initialization
    21.   // And uses that Type as the data type for meters as input
    22.   func (p *Person[S]) Move(meters S) {
    23.    p.DistanceMoved += meters
    24.   }
    25.   func (c *Car[S]) Move(meters S) {
    26.    c.DistanceMoved += meters
    27.   }
    28.   // Move is a generic function that takes in a Generic Moveable and moves it
    29.   func Move[S Subtractable, V Moveable[S]](v V, meters S "S Subtractable, V Moveable[S]") {
    30.    v.Move(meters)
    31.   }
    复制代码
    类型断言方案的 Move:
    1.  package benchmarking
    2.   // Below is the Type casting based Solution
    3.   //
    4.   type CarRegular struct {
    5.    Name          string
    6.    DistanceMoved int
    7.   }
    8.   type PersonRegular struct {
    9.    Name          string
    10.    DistanceMoved float32
    11.   }
    12.   func (p *PersonRegular) Move(meters float32) {
    13.    p.DistanceMoved += meters
    14.   }
    15.   func (c *CarRegular) Move(meters int) {
    16.    c.DistanceMoved += meters
    17.   }
    18.   func MoveRegular(v interface{}, distance float32) {
    19.    switch v.(type) {
    20.    case *PersonRegular:
    21.     v.(*PersonRegular).Move(distance)
    22.    case *CarRegular:
    23.     v.(*CarRegular).Move(int(distance))
    24.    default:
    25.     // Handle Unsupported types, not needed by Generic solution as Compiler does this for you
    26.    }
    27.   }
    复制代码
    现在我们已经有了解决方案,是时候开始基准测试了。我将在基准测试之前创建 Persons 和 Cars,我们将测量Move 和MoveRegular 的性能。
    1. package benchmarking
    2.   import "testing"
    3.   func Benchmark_Structures(b *testing.B) {
    4.    // Init the structs
    5.    p := &Person[float32]{Name: "John"}
    6.    c := &Car[int]{Name: "Ferrari"}
    7.    pRegular := &PersonRegular{Name: "John"}
    8.    cRegular := &CarRegular{Name: "Ferrari"}
    9.    // Run the test
    10.    b.Run("Person_Generic_Move", func(b *testing.B) {
    11.     for i := 0; i < b.N; i++ {
    12.      // generic will try to use float64 if we dont tell it is a float32
    13.      Move[float32](p, 10.2 "float32")
    14.     }
    15.    })
    16.    b.Run("Car_Generic_Move", func(b *testing.B) {
    17.     for i := 0; i < b.N; i++ {
    18.      Move(c, 10)
    19.     }
    20.    })
    21.    b.Run("Person_Regular_Move", func(b *testing.B) {
    22.     for i := 0; i < b.N; i++ {
    23.      MoveRegular(pRegular, 10.2)
    24.     }
    25.    })
    26.    b.Run("Car_Regular_Move", func(b *testing.B) {
    27.     for i := 0; i < b.N; i++ {
    28.      MoveRegular(cRegular, 10)
    29.     }
    30.    })
    31.   }
    复制代码
    我使用以下命令运行测试:
    1.  go test -v -bench=Benchmark_Structures -benchtime=1000000000x -count 5
    复制代码
    运行基准测试的结果:
    1. goos: windows
    2.   goarch: amd64
    3.   pkg: programmingpercy/benchgeneric
    4.   cpu: Intel(R) Core(TM) i7-10750H CPU @ 2.60GHz
    5.   Benchmark_Structures
    6.   Benchmark_Structures/Person_Generic_Move
    7.   Benchmark_Structures/Person_Generic_Move-4              1000000000               4.690 ns/op
    8.   Benchmark_Structures/Person_Generic_Move-4              1000000000               4.668 ns/op
    9.   Benchmark_Structures/Person_Generic_Move-4              1000000000               4.727 ns/op
    10.   Benchmark_Structures/Person_Generic_Move-4              1000000000               4.664 ns/op
    11.   Benchmark_Structures/Person_Generic_Move-4              1000000000               4.699 ns/op
    12.   Benchmark_Structures/Car_Generic_Move
    13.   Benchmark_Structures/Car_Generic_Move-4                 1000000000               3.176 ns/op
    14.   Benchmark_Structures/Car_Generic_Move-4                 1000000000               3.188 ns/op
    15.   Benchmark_Structures/Car_Generic_Move-4                 1000000000               3.296 ns/op
    16.   Benchmark_Structures/Car_Generic_Move-4                 1000000000               3.144 ns/op
    17.   Benchmark_Structures/Car_Generic_Move-4                 1000000000               3.156 ns/op
    18.   Benchmark_Structures/Person_Regular_Move
    19.   Benchmark_Structures/Person_Regular_Move-4              1000000000               4.694 ns/op
    20.   Benchmark_Structures/Person_Regular_Move-4              1000000000               4.634 ns/op
    21.   Benchmark_Structures/Person_Regular_Move-4              1000000000               4.677 ns/op
    22.   Benchmark_Structures/Person_Regular_Move-4              1000000000               4.660 ns/op
    23.   Benchmark_Structures/Person_Regular_Move-4              1000000000               4.626 ns/op
    24.   Benchmark_Structures/Car_Regular_Move
    25.   Benchmark_Structures/Car_Regular_Move-4                 1000000000               2.560 ns/op
    26.   Benchmark_Structures/Car_Regular_Move-4                 1000000000               2.555 ns/op
    27.   Benchmark_Structures/Car_Regular_Move-4                 1000000000               2.553 ns/op
    28.   Benchmark_Structures/Car_Regular_Move-4                 1000000000               2.579 ns/op
    29.   Benchmark_Structures/Car_Regular_Move-4                 1000000000               2.560 ns/op
    30.   PASS
    31.   ok      programmingpercy/benchgeneric   75.830s
    复制代码
    看到类型断言解决方案比泛型解决方案更快,我有点惊讶。我确保多次运行的基准测试,它不是偶然的。
      我们可以从基准中看到,基于 Cars 的 Int 解决方案都比基于 Person 的 float32 的更快。
      Person move 方法具有相同的性能,无论是泛型解决方案还是常规解决方案。但是,你可以看到 Cars 的不同之处,类型断言的 Cars 是最快的。类型断言执行比泛型快 20%。
      因此,该基准的关键要点如下。
      ·基于浮点的类型具有相同的性能,而类型断言的整数 cars 速度更快,这不是我的观点。
      · Float32 加法比 int 慢。
      结论
      所以,我们现在已经测试了一些我可以看到泛型有用的用例。
      老实说,我确实希望第二个基准也能证明泛型更快。这将进一步证明我的说法,即泛型由于是在编译时而不是运行时决定的,因此性能更高。
      通过使用泛型或特定于数据类型的函数,我们可以在第一个用例中看到相当大的性能提升。我知道几纳秒可能看起来很荒谬,但是在某些用例中,这些类型的极端优化很重要。我曾经做过一个高性能的网络嗅探器,它必须实时处理大量的网络数据。编写这样的软件将需要所有的优化。
      我们已经看到,选择正确的数据类型会对性能产生很大影响。但是,我认为我们可以说,那些表示担心泛型会拖慢软件速度的读者可以冷静下来。从好的方面来说,我看到泛型解决方案允许我们更轻松地交换数据类型,从而提高性能。
      另一方面,Go 中的类型断言和类型转换似乎具有超强的性能。
      正如我们所看到的,许多因素都会对结果产生影响,例如使用的算术运算符[5]、数据类型等。在我的基准测试中可能会出现我不知道的错误。









    分享到:  QQ好友和群QQ好友和群 QQ空间QQ空间 腾讯微博腾讯微博 腾讯朋友腾讯朋友
    收藏收藏
    回复

    使用道具 举报

    本版积分规则

    关闭

    站长推荐上一条 /1 下一条

    小黑屋|手机版|Archiver|51Testing软件测试网 ( 沪ICP备05003035号 关于我们

    GMT+8, 2024-5-7 09:51 , Processed in 0.079441 second(s), 23 queries .

    Powered by Discuz! X3.2

    © 2001-2024 Comsenz Inc.

    快速回复 返回顶部 返回列表