首页 星云 工具 资源 星选 资讯 热门工具
:

PDF转图片 完全免费 小红书视频下载 无水印 抖音视频下载 无水印 数字星空

使用Golang的协程竟然变慢了|100万个协程的归并排序耗时分析

编程知识
2024年09月01日 14:24

前言

这篇文章将用三个版本的归并排序,为大家分析使用协程排序的时间开销(被排序的切片长度由128到1000w)

本期demo地址:https://github.com/BaiZe1998/go-learning

往期视频讲解 📺:B站:白泽talk,公众号:白泽talk

image-20240726234405804

认为并发总是更快

  • 线程:OS 调度的基本单位,用于调度到 CPU 上执行,线程的切换是一个高昂的操作,因为要求将当前 CPU 中运行态的线程上下文保存,切换到可执行态,同时调度一个可执行态的线程到 CPU 中执行。
  • 协程:线程由 OS 上下文切换 CPU 内核,而 Goroutine 则由 Go 运行时上下文切换协程。Go 协程占用内存比线程少(2KB/2MB),协程的上下文切换比线程快80~90%。

🌟 GMP 模型:

  • G:Goroutine
    • 执行态:被调度到 M 上执行
    • 可执行态:等待被调度
    • 等待态:因为一些原因被阻塞
  • M:OS thread
  • P:CPU core
    • 每个 P 有一个本地 G 队列(任务队列)
    • 所有 P 有一个公共 G 队列(任务队列)

协程调度规则:每一个 OS 线程(M)被调度到 P 上执行,然后每一个 G 运行在 M 上。

image-20240224111521402

🌟 上图中展示了一个4核 CPU 的机器调度 Go 协程的场景:

此时 P2 正在闲置因为 M3 执行完毕释放了对 P2 的占用,虽然 P2 的 Local queue 中已经空了,没有 G 可以调度执行,但是每隔一定时间,Go runtime 会去 Global queue 和其他 P 的 local queue 偷取一些 G 用于调度执行(当前存在6个可执行的G)。

特别的,在 Go1.14 之前,Go 协程的调度是合作形式的,因此 Go 协程发生切换的只会因为阻塞等待(IO/channel/mutex等),但 Go1.14 之后,运行时间超过 10ms 的协程会被标记为可抢占,可以被其他协程抢占 P 的执行。

归并案例

🌟 为了印证有时候多协程并不一定会提高性能,这里以归并排序为例举三个例子:

image-20240224232145909

主函数:

func main() {
	size := []int{128, 512, 1024, 2048, 100000, 1000000, 10000000}
	sortVersion := []struct {
		name string
		sort func([]int)
	}{
		{"Mergesort V1", SequentialMergesortV1},
		{"Mergesort V2", SequentialMergesortV2},
		{"Mergesort V3", SequentialMergesortV3},
	}
	for _, s := range size {
		fmt.Printf("Testing Size: %d\n", s)
		o := GenerateSlice(s)
		for _, v := range sortVersion {
			s := make([]int, len(o))
			copy(s, o)
			start := time.Now()
			v.sort(s)
			elapsed := time.Since(start)
			fmt.Printf("%s: %s\n", v.name, elapsed)
		}
		fmt.Println()
	}
}

v1的实现:

func sequentialMergesort(s []int) {
    if len(s) <= 1 {
    	return
    }
    middle := len(s) / 2
    sequentialMergesort(s[:middle])
    sequentialMergesort(s[middle:])
    merge(s, middle)
}

func merge(s []int, middle int) {
    // ...
}

v2的实现:

func SequentialMergesortV2(s []int) {
	if len(s) <= 1 {
		return
	}
	middle := len(s) / 2

	var wg sync.WaitGroup
	wg.Add(2)

	go func() {
		defer wg.Done()
		SequentialMergesortV2(s[:middle])
	}()
	go func() {
		defer wg.Done()
		SequentialMergesortV2(s[middle:])
	}()
	wg.Wait()
	Merge(s, middle)
}

v3的实现:

const max = 2048

func SequentialMergesortV3(s []int) {
	if len(s) <= 1 {
		return
	}
	if len(s) < max {
		SequentialMergesortV1(s)
	} else {
		middle := len(s) / 2

		var wg sync.WaitGroup
		wg.Add(2)

		go func() {
			defer wg.Done()
			SequentialMergesortV3(s[:middle])
		}()
		go func() {
			defer wg.Done()
			SequentialMergesortV3(s[middle:])
		}()

		wg.Wait()
		Merge(s, middle)
	}
}

耗时对比:

Testing Size: 128
Mergesort V1: 10.583µs
Mergesort V2: 211.25µs
Mergesort V3: 7.5µs

Testing Size: 512
Mergesort V1: 34.208µs
Mergesort V2: 500.666µs
Mergesort V3: 60.291µs

Testing Size: 1024
Mergesort V1: 48.666µs
Mergesort V2: 643.583µs
Mergesort V3: 47.084µs

Testing Size: 2048
Mergesort V1: 94.666µs
Mergesort V2: 786.125µs
Mergesort V3: 77.667µs

Testing Size: 100000
Mergesort V1: 6.810917ms
Mergesort V2: 58.148291ms
Mergesort V3: 4.219375ms

Testing Size: 1000000
Mergesort V1: 62.053875ms
Mergesort V2: 558.586458ms
Mergesort V3: 37.99375ms

Testing Size: 10000000
Mergesort V1: 632.3875ms
Mergesort V2: 5.764997041s
Mergesort V3: 388.343584ms

由于创建协程和调度协程本身也有开销,第二种情况无论多少个元素都使用协程去进行并行排序,导致归并很少的元素也需要创建协程和调度,开销比排序更多,导致性能还比不上第一种顺序归并。

特别在切片长度为1000w的时候,基于v2创建的协程数量共计百万。

而在本台电脑上,经过调试第三种方式可以获得比第一种方式更优的性能,因为它在元素大于2048个的时候,选择并行排序,而少于则使用顺序排序。但是2048是一个魔法数,不同电脑上可能不同。这里这是为了证明,完全依赖并发/并行的机制,并不一定会提高性能,需要注意协程本身的开销。

小结

可以克隆项目之后,使用协程池写一个归并排序,进一步比较内存消耗。

From:https://www.cnblogs.com/YLTFY1998/p/18391320
本文地址: http://www.shuzixingkong.net/article/1630
0评论
提交 加载更多评论
其他文章 如何实现一个通用的接口限流、防重、防抖机制
介绍 最近上了一个新项目,考虑到一个问题,在高并发场景下,我们无法控制前端的请求频率和次数,这就可能导致服务器压力过大,响应速度变慢,甚至引发系统崩溃等严重问题。为了解决这些问题,我们需要在后端实现一些机制,如接口限流、防重复提交和接口防抖,而这些是保证接口安全、稳定提供服务,以及防止错误数据 和
一种优雅的方式整合限流、幂等、防盗刷
大家在工作中肯定遇到过接口被人狂刷的经历,就算没有经历过,在接口开发的过程中,我们也需要对那些容易被刷的接口或者和会消耗公司金钱相关的接口增加防盗刷功能。例如,发送短信接口以及发送邮件等接口,我看了国内很多产品的短信登录接口,基本上都是做了防盗刷,如果不做的话,一夜之间,也许公司都赔完了┭┮﹏┭┮。
一种优雅的方式整合限流、幂等、防盗刷 一种优雅的方式整合限流、幂等、防盗刷 一种优雅的方式整合限流、幂等、防盗刷
gcc/g++编译
编译工具链 我们写程序的时候用的都是集成开发环境 (IDE: Integrated Development Environment),集成开发环境可以极大地方便我们程序员编写程序,但是配置起来也相对麻烦。在 Linux 环境下,我们用的是编译工具链,又叫软件开发工具包(SDK:Software De
gcc/g++编译 gcc/g++编译 gcc/g++编译
Dify大语言模型应用开发平台新手必备:安装注册与私有服务器部署全步骤
Dify简介 Dify是一个开源的大语言模型(Large Language Model, LLM)应用开发平台。它融合了后端即服务(Backend as a Service, BaaS)和LLMOps的理念,旨在帮助开发者,甚至是非技术人员,能够快速搭建和部署生成式AI应用程序。 Dify的主要特点
Dify大语言模型应用开发平台新手必备:安装注册与私有服务器部署全步骤 Dify大语言模型应用开发平台新手必备:安装注册与私有服务器部署全步骤 Dify大语言模型应用开发平台新手必备:安装注册与私有服务器部署全步骤
Python自动复制Excel数据:将各行分别重复指定次数
本文介绍基于Python语言,读取Excel表格文件数据,并将其中符合我们特定要求的那一行加以复制指定的次数,而不符合要求的那一行则不复制;并将所得结果保存为新的Excel表格文件的方法~
Python自动复制Excel数据:将各行分别重复指定次数 Python自动复制Excel数据:将各行分别重复指定次数 Python自动复制Excel数据:将各行分别重复指定次数
iptables 工作过程整理
转载注明出处: 1.概念和工作原理 iptables是Linux系统中用来配置防火墙的命令。iptables是工作在TCP/IP的二、三、四层,当主机收到一个数据包后,数据包先在内核空间处理,若发现目标地址是自身,则传到用户空间中交给对应的应用程序处理,若发现目标不是自身,则会将包丢弃或进行转发。
iptables 工作过程整理 iptables 工作过程整理
Go plan9 汇编:手写汇编
原创文章,欢迎转载,转载请注明出处,谢谢。 0. 前言 在 Go plan9 汇编: 打通应用到底层的任督二脉 一文中介绍了从应用程序到汇编指令的转换。本文将结合汇编和 Go 程序实现手写基本的汇编指令,以加深对 Go plan9 汇编的了解。 1. 手写汇编 1.1 全局变量 首先写一个打印整型变
Go plan9 汇编:手写汇编 Go plan9 汇编:手写汇编 Go plan9 汇编:手写汇编
Gluon 编译 JavaFx -> android apk
Gluon 编译 JavaFx -&gt; android apk 本文的内容属 在linux服务器上 搭建 Gluon 编译 android-apk 环境 这一篇文章直接跟着官网操作一次性成功 虚拟机版本 centos8 Architecture: x86-64 开始安装相关前置工具 gcc ve
Gluon 编译 JavaFx -> android apk