使用unsafe与协程简单分析性能

本文涉及的产品
函数计算FC,每月15万CU 3个月
云原生网关 MSE Higress,422元/月
可观测链路 OpenTelemetry 版,每月50GB免费额度
简介: 【5月更文挑战第18天】本文档探讨了Go语言中使用标准库`unsafe`包与语言内置方式在类型转换(特别是`string`与`[]byte`之间)的性能差异。在涉及内存分配和复制的场景下,`unsafe`包能显著提升效率,但需深入理解其工作原理。

1 简介

本节专门使用多例程的方式充分利用cpu,以对比使用内置标准库unsafe包和语言层面的方式转换字符。

question_ans.png

通过基准测试发现,对于string[]byteunsafe包实现避免了内存复制,性能更优;

[]bytestring两者差异较小。在float64uint64的转换上,两种方法性能接近。

该例子的测试还展示了多CPU环境下转换函数的性能变化。

2 实例高效类型转换 性能对比

我们实现go语言的类型转换,和 基于 unsafe的类型转换,并进行benchmark基准性能测试。

string类型变量是不可变类型,而[]byte为可变类型,当转换为[]byte时,go需要从系统申请一块新内存,

并将string类型变量值赋予这个新的内存,通过下面基于unsafe包的实现,不需要额外的内存复制:

转换后的[]byte变量与输入参数的string类型变量共享底层存储(并不能修改返回的切片就能改变原字符串。)

而将[]byte转换为string则简单一些,因为[]byte内部时一个三元组ptr,len,cap, 在string内部为 ptr,len。

只需要通过Pointer将[]byte内部重新解释为string的内部表示。

在这里,只是把 "hello, world!" 这字符串转string换为 []byte 或 把 []byte转换为字符串 string

    var benchStr string = "hello, world!"

    func B2String(b []byte) string {

        return string(b)
    }

    func S2Bytes(s string) []byte {
        return []byte(s)
    }

    func BenchmarkB2String(b *testing.B) {
        var sbe []byte = []byte(benchStr)
        for n := 0; n < b.N; n++ {
            B2String(sbe) 
        }
    }

    func BenchmarkS2Bytes(b *testing.B) {
        var sbe string = benchStr
        for n := 0; n < b.N; n++ {
            S2Bytes(sbe) 
        }
    }

    func Bytes2String(b []byte) string {
        return *(*string)(unsafe.Pointer(&b))
    }

    func String2Bytes(s string) []byte {
        sh := (*reflect.StringHeader)(unsafe.Pointer(&s))
        bh := reflect.SliceHeader{
            Data: sh.Data,
            Len:  sh.Len,
            Cap:  sh.Len,
        }
        return *(*[]byte)(unsafe.Pointer(&bh))
    }

    func BenchmarkBytes2String(b *testing.B) {
        var sbe []byte = []byte(benchStr)
        for n := 0; n < b.N; n++ {
            Bytes2String(sbe) 
        }
    }
    func BenchmarkString2Bytes(b *testing.B) {
        var sbe string = benchStr
        for n := 0; n < b.N; n++ {
            String2Bytes(sbe) 
        }
    }

3 float64 和 uint64 转换没有太大区别

float64 到 uint64的转换,由于没有内存的重新分配和复制操作, 使用go语言函数和 unsafe包没有太多区别。

    goos: linux
    goarch: amd64
    cpu: AMD Ryzen 5 3500U with Radeon Vega Mobile Gfx
    BenchmarkFloat64bits
    BenchmarkFloat64bits-2          1000000000               0.3591 ns/op
    BenchmarkFloat64bits-2          1000000000               0.3238 ns/op
    BenchmarkFloat64bits-4          1000000000               0.3271 ns/op
    BenchmarkFloat64bits-4          1000000000               0.3104 ns/op
    BenchmarkFloat64bits-8          1000000000               0.3144 ns/op
    BenchmarkFloat64bits-8          1000000000               0.3318 ns/op
    BenchmarkGoFloat64bits
    BenchmarkGoFloat64bits-2        1000000000               0.3072 ns/op
    BenchmarkGoFloat64bits-2        1000000000               0.2978 ns/op
    BenchmarkGoFloat64bits-4        1000000000               0.3185 ns/op
    BenchmarkGoFloat64bits-4        1000000000               0.3242 ns/op
    BenchmarkGoFloat64bits-8        1000000000               0.2980 ns/op
    BenchmarkGoFloat64bits-8        1000000000               0.3265 ns/op
    BenchmarkGouint64bits
    BenchmarkGouint64bits-2         1000000000               0.3019 ns/op
    BenchmarkGouint64bits-2         1000000000               0.3115 ns/op
    BenchmarkGouint64bits-4         1000000000               0.3227 ns/op
    BenchmarkGouint64bits-4         1000000000               0.2969 ns/op
    BenchmarkGouint64bits-8         1000000000               0.3016 ns/op
    BenchmarkGouint64bits-8         1000000000               0.3229 ns/op
    PASS
    ok      command-line-arguments  6.546s

4 多cpu执行和分析结果

指令执行

   go test -v -count 2 -bench .  bytestr_change_bench_test.go   -cpu 2,4,8 >l.txt

在linux 执行结果如下:

    goos: linux
    goarch: amd64
    cpu: AMD Ryzen 5 3500U with Radeon Vega Mobile Gfx

使用go函数,转换字符 hello world, 从 byte 到string

    BenchmarkB2String
    BenchmarkB2String-2             241006406                4.974 ns/op
    BenchmarkB2String-2             246260128                5.265 ns/op
    BenchmarkB2String-4             204571192                5.306 ns/op
    BenchmarkB2String-4             227578519                5.266 ns/op
    BenchmarkB2String-8             224398243                5.691 ns/op
    BenchmarkB2String-8             230737006                5.544 ns/op

使用go函数 转换字符 hello world, 从 string到byte

    BenchmarkS2Bytes
    BenchmarkS2Bytes-2              240975625                5.047 ns/op
    BenchmarkS2Bytes-2              180348247                5.797 ns/op
    BenchmarkS2Bytes-4              233383180                4.889 ns/op
    BenchmarkS2Bytes-4              257518407                5.167 ns/op
    BenchmarkS2Bytes-8              245337022                6.379 ns/op
    BenchmarkS2Bytes-8              124589875                8.548 ns/op

使用usafe包转换字符 hello world, 从 byte 到string

    BenchmarkBytes2String
    BenchmarkBytes2String-2         1000000000               1.035 ns/op
    BenchmarkBytes2String-2         1000000000               1.009 ns/op
    BenchmarkBytes2String-4         1000000000               0.7254 ns/op
    BenchmarkBytes2String-4         1000000000               0.8996 ns/op
    BenchmarkBytes2String-8         1000000000               0.7063 ns/op
    BenchmarkBytes2String-8         1000000000               0.7204 ns/op

使用usafe包转换字符 hello world, 从 string到byte

    BenchmarkString2Bytes
    BenchmarkString2Bytes-2         1000000000               0.4363 ns/op
    BenchmarkString2Bytes-2         1000000000               0.3780 ns/op
    BenchmarkString2Bytes-4         1000000000               0.3678 ns/op
    BenchmarkString2Bytes-4         1000000000               0.3154 ns/op
    BenchmarkString2Bytes-8         1000000000               0.3109 ns/op
    BenchmarkString2Bytes-8         1000000000               0.3334 ns/op
    PASS
    ok      command-line-arguments  32.022s

这个例子中通过 -cpu 2,4,8 指令选项告知 go test 将每个性能基准测试函数分别在 GOMAXPROCS 等于 2,4,8的场景各运行一次。

从输出结果看,我们可以很容易看出不同被测试函数的性能随着 GOMAXPROCS增大后的性能变化。

每次执行基准测试,go test 都会启动 GOMAXPROCS 数量的新 goroutine,这些 goroutine 共同执行 b.N 次循环。

每个goroutine 尽量均衡分担循环次数。

这里输出的内存分配信息告诉我们,op(代表每次循环操作) 每一次执行 该值表示 函数在基准测试中 for 循环每次平均执行时间。

    BenchmarkString2Bytes-8         1000000000               0.3334 ns/op

也就是函数 BenchmarkString2Bytes 的例程 -8 每次for循环0.3334 ns 执行时间。

    BenchmarkS2Bytes-8              124589875                8.548 ns/op

也就是函数 BenchmarkS2Bytes 的例程 -8 每次for循环 8.548 ns 平均执行时间。

在不同平台执行速度相差不大。

但是从结果直观地看,go语言函数转换和unsafe包转换都在 ns 级别。

使用unsafe的转换效率最高快 25倍,也就是8.548 : 0.3334。

5 小结

得到的结果是,在特定场合比如需要内存分配和复制的场景,内置库可以大幅度提高转换效率。

这是建立在熟练操作和理解其过程的原理的基础上。
后面几个章节,将从更多例子和原理尝试解释它。

目录
相关文章
|
3月前
|
NoSQL Unix 编译器
Golang协程goroutine的调度与状态变迁分析
文章深入分析了Golang中goroutine的调度和状态变迁,包括Grunnable、Gwaiting、Grunning和Gsyscall等状态,以及它们之间的转换条件和原理,帮助理解Go调度器的内部机制。
44 0
|
4月前
|
大数据 数据处理 API
性能飞跃:Python协程与异步函数在数据处理中的高效应用
【7月更文挑战第15天】在大数据时代,Python的协程和异步函数解决了同步编程的性能瓶颈问题。同步编程在处理I/O密集型任务时效率低下,而Python的`asyncio`库支持的异步编程利用协程实现并发,通过`async def`和`await`避免了不必要的等待,提升了CPU利用率。例如,从多个API获取数据,异步方式使用`aiohttp`并发请求,显著提高了效率。掌握异步编程对于高效处理大规模数据至关重要。
52 4
|
6月前
|
程序员 Python
Python并发编程之协程与多线程对比分析
本文通过对Python中协程和多线程的特点、优缺点以及适用场景进行深入比较分析,帮助读者更好地理解并发编程中不同技术方案的选择与应用。
|
6月前
|
数据处理 UED 开发者
Python并发编程之协程与多线程对比分析
本文将从Python并发编程的角度出发,对比分析协程与多线程两种并发处理方式的优缺点及适用场景,帮助读者更好地选择适合自己项目的并发方案。
|
6月前
|
开发者 Python
深入浅出Python协程:提高并发性能的利器
本文旨在深入探讨Python中的协程机制,一种轻量级的并发编程解决方案。与传统的多线程和多进程相比,协程提供了更高效的并发性能,尤其是在I/O密集型应用中。我们将从协程的基本概念入手,解析其工作原理,并通过实例讲解如何在Python中使用协程来优化程序性能。文章还将对比协程与其他并发模型的优缺点,帮助读者全面理解协程在现代软件开发中的应用价值。
52 3
|
6月前
|
API 调度 开发者
深入浅出Python协程:提高并发性能的秘诀
在当今快速发展的互联网时代,软件系统面临着越来越多的并发处理需求。本文将深入探讨Python中的协程(Coroutine)概念,它作为一种轻量级线程,通过优雅地在单个线程内部进行任务切换,实现高效的IO操作。本文不仅将介绍协程的基础知识和工作原理,还会通过实例演示如何在Python项目中应用协程来提高并发性能,最后将对比协程与传统多线程、多进程模型的优缺点,帮助读者更好地理解协程在现代编程中的重要性。
105 3
|
6月前
|
开发者 Python
深入理解Python协程:提高并发性能的关键
在本文中,我们将探索Python中的协程(coroutine)机制,这是一种比线程更轻量级的并发执行单元。通过深入分析协程的工作原理、如何使用以及它们如何帮助提高应用程序的并发性能,本文旨在为读者提供一个全面的理解。不同于传统的技术文章摘要,我们不仅简述内容,还承诺将带领读者通过实际代码示例,深入浅出地掌握协程的强大之处。无论你是初学者还是有经验的开发者,本文都将为你打开并行编程的新视界。
|
6月前
|
Python
深入浅出Python协程:提升并发性能的艺术
本文旨在深入探讨Python协程的内部机制及其在提升程序并发性能中的关键作用。不同于传统的摘要,我们将通过一个简洁明了的比喻来揭示协程的本质:想象一个高效的快餐厨房,厨师(主线程)在准备一个订单(任务)时,如果需要等待某个步骤(如烤面包),他会转而开始准备下一个订单,而不是站在那里等待。这样,厨房的整体效率得到了极大提升。Python协程正是这样一种机制,它允许代码在等待操作(如I/O操作)完成时“挂起”,转而执行其他任务,从而显著提高并发性能。本文将通过示例和解析,带你一步步深入理解协程的工作原理,以及如何在你的Python项目中有效地利用协程来提升性能。
70 1
|
6月前
|
数据处理 数据库 Python
深入浅出Python协程:提升并发性能的秘诀
在当今高速发展的互联网时代,面对海量数据处理和高并发请求的挑战,传统的同步编程模式已难以满足现代软件开发的需求。Python作为一门强大的编程语言,其协程(Coroutine)功能为解决这一问题提供了优雅的方案。本文将从协程的概念入手,通过实例详解如何在Python中使用协程来实现异步编程,进而提升程序的并发性能。我们将探讨协程的工作原理、与线程的区别,以及如何在实际项目中合理利用协程来优化性能。通过本文的学习,读者将能够掌握Python协程的核心概念和应用技巧,为构建高效、可扩展的应用程序打下坚实的基础。
61 1
|
6月前
|
程序员 测试技术 数据处理
Python中的装饰器应用与实现Python并发编程之协程与多线程对比分析
在Python编程中,装饰器是一种强大的工具,能够简洁而优雅地扩展函数或类的功能。本文将深入探讨Python中装饰器的原理、应用场景以及实现方法,帮助读者更好地理解和运用这一重要的编程概念。 本文将从Python并发编程的角度出发,对比分析协程与多线程两种并发处理方式的优缺点及适用场景,帮助读者更好地选择适合自己项目的并发方案。