分配
概览
在服务器端 Swift 应用程序中,内存分配对于各种任务至关重要,例如创建对象、操作数据结构和管理资源。Swift 会根据需要分配内存资源,并提供内置的内存管理机制,例如自动引用计数 (ARC),来处理分配、释放和内存所有权。
分配有助于优化内存使用,为每个对象或数据结构分配精确数量的内存,从而减少内存浪费并提高应用程序性能。但是,可以填充 Swift 分配以强制执行需要硬件高效访问的数据类型或结构的内存对齐要求,从而降低未对齐内存访问问题的风险并提高性能。
此外,适当的分配管理可以防止内存泄漏,并确保在不再需要内存时将其释放。这有助于维护服务器应用程序的稳定性和可靠性。
堆栈
一般来说,Swift 有两个用于内存分配的基本位置:堆和栈。
Swift 自动在堆或栈数据结构中分配内存。
对于 Swift 中的高性能软件,了解堆分配的来源并减少软件提供的分配数量至关重要。识别这些问题类似于识别其他性能问题,例如
- 在优化性能之前,资源分配在哪里?
- 使用了哪些类型的资源?CPU?内存?堆分配?
注意:虽然堆分配在计算开销方面可能相对昂贵,但它们提供了灵活性和动态内存管理功能,这对于处理可变大小或动态数据结构等任务至关重要。
性能分析
您可以根据项目的具体要求,使用不同的工具和技术来分析您的 Swift 代码。一些常用的性能分析技术包括
- 使用操作系统供应商提供的性能分析工具,例如 macOS 上的 Instruments 或 Linux 上的
perf
。 - 使用手动计时测量,例如在关键代码段前后添加时间戳。
- 利用 Swift 的性能分析库和框架,例如 SwiftMetrics 或 XCGLogger。
对于 macOS,您可以使用 Allocations instrument 在 Xcode Instruments 中帮助您分析和优化应用程序中的内存使用情况。Allocations instrument 跟踪所有堆和匿名虚拟内存分配的大小和数量,并按类别组织它们。
如果您的生产工作负载在 Linux 而不是 macOS 上运行,则分配的数量可能会因您的设置而异。
本文档主要关注堆分配的数量,而不是它们的大小。
入门
Swift 的优化器在 release
模式下生成更快的代码并分配更少的内存。通过在 release
模式下分析您的 Swift 代码并根据结果进行优化,您可以提高应用程序的性能和效率。
请按照以下步骤操作
步骤 1. 通过运行此命令在 release
模式下构建您的代码
swift run -c release
步骤 2. 安装 perf
以分析您的代码环境,从而收集与性能相关的数据并优化 Swift 服务器应用程序的性能。
步骤 3. 克隆 FlameGraph 项目以生成火焰图可视化,帮助您快速识别代码库中的热点,可视化调用路径,了解执行流程并优化性能。要生成火焰图,您需要将 FlameGraph
存储库克隆到您的机器或容器中,使其在 ~/FlameGraph
中可用。
运行此命令以在 ~/FlameGraph
中克隆 https://github.com/brendangregg/FlameGraph
存储库
git clone https://github.com/brendangregg/FlameGraph
在 Docker 中运行时,使用此命令将 FlameGraph
存储库绑定挂载到容器中
docker run -it --rm \
--privileged \
-v "/path/to/FlameGraphOnYourMachine:/FlameGraph:ro" \
-v "$PWD:PWD" -w "$PWD" \
swift:latest
通过直观地突出显示最常调用的函数或占用最多处理时间的函数,您可以将优化工作集中在提高关键代码路径的性能上。
工具
您可以使用 Linux perf
工具识别需要优化的区域,并做出明智的决策,以提高 Swift 服务器代码的性能和效率。
perf
工具是 Linux 系统上可用的性能分析和分析工具。虽然它不是 Swift 专用的,但对于分析服务器上 Swift 代码的性能非常有用,原因如下
- 低开销,这意味着它可以收集性能数据,而对 Swift 代码的执行影响极小。
- 丰富的功能集,例如 CPU 分析、内存分析和基于事件的采样。
- 火焰图生成,帮助您了解代码不同区域花费的相对时间,并识别性能瓶颈。
- 系统级分析,收集内核级别的性能数据,分析系统范围的事件,并了解其他进程或系统组件对 Swift 应用程序性能的影响。
- 灵活性和可扩展性,允许您自定义要分析的事件类型、设置采样率、指定过滤器等等。
提示 1:如果您在 Docker 容器中运行
perf
,您将需要一个特权容器,以提供必要的权限和访问权限,以便该工具收集性能数据。
提示 2:如果需要
root
访问权限,请在命令前加上sudo
。有关更多信息,请参阅 使perf
工作。
安装 perf 用户探针
如前所述,本文档的示例程序侧重于计算分配的数量。
大多数分配在 Linux 上使用 Swift 程序的 malloc
函数。在分配函数上安装 perf
用户探针可以提供有关何时调用分配函数的信息。
在本例中,为所有分配函数安装了用户探针,因为 Swift 使用其他函数,如 calloc
和 posix_memalign
。
# figures out the path to libc
libc_path=$(readlink -e /lib64/libc.so.6 /lib/x86_64-linux-gnu/libc.so.6)
# delete all existing user probes on libc (instead of * you can also list them individually)
perf probe --del 'probe_libc:*'
# installs a probe on `malloc`, `calloc`, and `posix_memalign`
perf probe -x "$libc_path" --add malloc --add calloc --add posix_memalign
随后,每当调用其中一个分配函数时,perf
中的事件都会触发。
输出应如下所示
Added new events:
probe_libc:malloc (on malloc in /usr/lib/x86_64-linux-gnu/libc-2.31.so)
probe_libc:calloc (on calloc in /usr/lib/x86_64-linux-gnu/libc-2.31.so)
probe_libc:posix_memalign (on posix_memalign in /usr/lib/x86_64-linux-gnu/libc-2.31.so)
[...]
在这里,您可以看到每当调用相应的函数时,perf
都会触发新事件 probe_libc:malloc
;probe_libc:calloc
。
要确认用户探针 probe_libc:malloc
工作正常,请运行此命令
perf stat -e probe_libc:malloc -- bash -c 'echo Hello World'
输出应与此类似
Hello World
Performance counter stats for 'bash -c echo Hello World':
1021 probe_libc:malloc
0.003840500 seconds time elapsed
0.000000000 seconds user
0.003867000 seconds sys
在本例中,用户探针似乎调用了分配函数 1021 次。
重要提示:如果探针调用分配函数的次数为 0 次,则表示存在错误。
运行分配分析
通过运行分配分析,您可以更好地了解应用程序中的内存使用模式,并识别和修复内存问题(例如泄漏或低效使用),最终提高代码的性能和稳定性。
示例程序
确认 malloc
上的用户探针工作正常后,您可以分析程序的分配。例如,您可以分析一个使用 AsyncHTTPClient 执行十个后续 HTTP 请求的程序。
分析使用 AsyncHTTPClient 的程序可以帮助优化其性能、改进错误处理、确保适当的并发和线程处理、增强代码可读性和可维护性以及评估可伸缩性考虑因素。
这是一个带有以下依赖项的程序源代码示例
dependencies: [
.package(url: "https://github.com/swift-server/async-http-client.git", from: "1.3.0"),
.package(url: "https://github.com/apple/swift-nio.git", from: "2.29.0"),
.package(url: "https://github.com/apple/swift-log.git", from: "1.4.2"),
],
使用 AsyncHTTPClient 的示例程序可以编写为
import AsyncHTTPClient
import NIO
import Logging
let urls = Array(repeating:"http://httpbin.org/get", count: 10)
var logger = Logger(label: "ahc-alloc-demo")
logger.info("running HTTP requests", metadata: ["count": "\(urls.count)"])
MultiThreadedEventLoopGroup.withCurrentThreadAsEventLoop { eventLoop in
let httpClient = HTTPClient(eventLoopGroupProvider: .shared(eventLoop),
backgroundActivityLogger: logger)
func doRemainingRequests(_ remaining: ArraySlice<String>,
overallResult: EventLoopPromise<Void>,
eventLoop: EventLoop) {
var remaining = remaining
if let first = remaining.popFirst() {
httpClient.get(url: first, logger: logger).map { [remaining] _ in
eventLoop.execute { // for shorter stacks
doRemainingRequests(remaining, overallResult: overallResult, eventLoop: eventLoop)
}
}.whenFailure { error in
overallResult.fail(error)
}
} else {
return overallResult.succeed(())
}
}
let promise = eventLoop.makePromise(of: Void.self)
// Kick off the process
doRemainingRequests(urls[...],
overallResult: promise,
eventLoop: eventLoop)
promise.futureResult.whenComplete { result in
switch result {
case .success:
logger.info("all HTTP requests succeeded")
case .failure(let error):
logger.error("HTTP request failure", metadata: ["error": "\(error)"])
}
httpClient.shutdown { maybeError in
if let error = maybeError {
logger.error("AHC shutdown failed", metadata: ["error": "\(error)"])
}
eventLoop.shutdownGracefully { maybeError in
if let error = maybeError {
logger.error("EventLoop shutdown failed", metadata: ["error": "\(error)"])
}
}
}
}
}
logger.info("exiting")
如果将程序作为 Swift 软件包运行,请首先使用此命令在 release
模式下编译它
swift build -c release
应该呈现名为 .build/release/your-program-name
的二进制文件,并且可以对其进行分析以获取分配的数量。
计数分配
计数分配并将它们可视化为图形可以帮助您分析内存利用率、分析内存使用情况、优化性能、重构和优化代码,以及调试程序中与内存相关的问题。
在将分配可视化为火焰图之前,首先使用二进制文件进行分析,以通过运行以下命令来获取分配数量
perf stat -e 'probe_libc:*' -- .build/release/your-program-name
此命令指示 perf
运行您的程序并计算用户探针 probe_libc:malloc
在您的应用程序中被命中或分配内存的次数。
输出应与此类似
Performance counter stats for '.build/release/your-program-name':
68 probe_libc:posix_memalign
35 probe_libc:calloc_1
0 probe_libc:calloc
2977 probe_libc:malloc
[...]
在本例中,程序通过 malloc
分配了 2977 次,并通过其他分配函数少量分配。
重要的是要注意,使用了 -e probe_libc:*
命令,而不是单独列出每个事件,例如
-e probe_libc: malloc
probe_libc:calloc
probe_libc:calloc_1
probe_libc:posix_memalign
提示:此方法假定您没有安装其他
perf
用户探针。如果安装了其他perf
用户探针,则需要单独指定要使用的每个事件。
收集原始数据
收集原始数据对于获得系统行为的准确表示、执行详细的性能分析和调试、分析趋势、实现分析灵活性以及指导性能优化工作至关重要。
perf
命令不允许在程序运行时创建实时图形。但是,Linux Perf 工具 提供了一个 perf record
实用程序命令,用于捕获性能事件以供以后分析。然后可以将收集的数据转换为图形。
通常,命令 perf record
可用于运行程序和 libc_probe:malloc
以收集信息,如下所示
perf record --call-graph dwarf,16384 \
-m 50000 \
-e 'probe_libc:*' -- \
.build/release/your-program-name
分解此命令可提供以下构造
perf record
命令指示perf
记录数据。--call-graph dwarf,16384
命令指示perf
使用 使用属性记录格式 (DWARF) 进行调试 信息来创建调用图。它还将最大堆栈转储大小设置为 16k,这应该足以提供完整的堆栈跟踪信息。- 虽然使用 DWARF 速度较慢(见下文),但它会创建最佳调用图。
-m 50000
表示perf
使用的环形缓冲区的大小,并以PAGE_SIZE
的倍数(通常为 4kB)输出。- 使用 DWARF 时,需要一个足够大的缓冲区以防止数据丢失。
-e 'probe_libc:*'
在malloc
;calloc
和其他malloc/calloc/...
用户探针触发时记录数据。- 当探针被触发或执行时,会发生触发事件,从而捕获有关分配的相关信息,以进行进一步的分析和调试。
您的程序输出应与此类似
<your program's output>
[ perf record: Woken up 2 times to write data ]
[ perf record: Captured and wrote 401.088 MB perf.data (49640 samples) ]
通过在代码库中的战略点放置用户探针,您可以跟踪和记录分配事件,以深入了解内存分配模式、识别潜在的性能问题或内存泄漏,并分析应用程序中的内存使用情况。
重要提示:如果
perf
输出返回lost chunks
并发出check the IO/CPU overload!
请求,请参阅下面的克服数据块丢失。
创建火焰图
一旦你使用 perf record
成功记录了数据,你可以调用以下命令来生成带有火焰图的 SVG 文件
perf script | \
/FlameGraph/stackcollapse-perf.pl - | \
swift demangle --simplified | \
/FlameGraph/flamegraph.pl --countname allocations \
--width 1600 > out.svg
以下是此命令结构的分解
perf script
命令将二进制信息放入perf record
捕获的文本形式中。stackcollapse-perf
命令将perf script
生成的堆栈转换为火焰图的正确格式。swift demangle --simplified
命令将符号名称转换为人类可读的格式。- 最后两个命令基于分配数量创建火焰图。
命令完成后,将生成一个 SVG 文件,你可以在浏览器中打开它。
注意:根据数据大小、算法复杂性、资源限制(如 CPU 功率或内存)、优化不良或效率低下的代码、外部服务、API 或网络延迟导致速度减慢,可能会导致运行时间过长。
读取火焰图
此火焰图是本节示例程序的直接结果。将鼠标悬停在堆栈帧上以获取更多信息,或单击任何堆栈帧以放大子树。
-
在解释火焰图时,X 轴表示计数而不是时间。堆栈的排列(左侧或右侧)不是由该堆栈的活动时间决定的,这与火焰图表不同。
- 此火焰图不是 CPU 火焰图,而是分配火焰图,其中一个样本表示一次分配,而不是花费在 CPU 上的时间。
-
宽堆栈帧不(一定)直接分配,这意味着该函数或该函数调用的某些内容分配了多次。
- 例如,
BaseSocketChannel.readable
是一个宽帧,但其函数不直接分配。相反,它调用了其他函数,例如 SwiftNIO 和 AsyncHTTPClient 的其他部分,这些函数分配了相当多的内存。
- 例如,
macOS 上的分配火焰图
虽然本教程的大部分内容都集中在 perf
工具上,但你可以使用 macOS 创建相同的图表。
步骤 1. 要开始使用,请通过运行以下命令,使用 DTrace 框架收集原始数据
sudo dtrace -n 'pid$target::malloc:entry,pid$target::posix_memalign:entry,pid$target::calloc:entry,pid$target::malloc_zone_malloc:entry,pid$target::malloc_zone_calloc:entry,pid$target::malloc_zone_memalign:entry { @s[ustack(100)] = count(); } ::END { printa(@s); }' -c .build/release/your-program > raw.stacks
与 Linux 的 perf
用户探针类似,DTrace 也使用探针。之前的命令指示 dtrace
聚合对分配函数等效项的调用次数
malloc
posix_memalign
calloc
malloc_zone_*
注意:在 Apple 平台上,Swift 使用的分配函数略多于 Linux。
步骤 2. 收集数据后,运行以下命令以创建 SVG 文件
cat raw.stacks |\
/FlameGraph/stackcollapse.pl - | \
swift demangle --simplified | \
/FlameGraph/flamegraph.pl --countname allocations \
--width 1600 > out.svg
你会注意到此命令与 perf
调用类似,除了
cat raw.stacks
命令替换了perf script
命令,因为dtrace
已经包含了一个文本数据文件。stackcollapse.pl
命令(解析dtrace
聚合输出)替换了stackcollapse-perf.pl
命令(解析perf script
输出)。
其他 perf 技巧
Swift 的分配模式
根据火焰图提供的信息,优化内存分配并提高代码效率可以帮助你使 Swift 代码更高效且更具视觉吸引力。Swift 中分配的形状可能因分配的内存类型及其使用方式而异。
Swift 中一些常见的分配形状包括
- 单对象分配
- 集合分配
- 字符串
- 函数调用堆栈
- 协议存在类型
- 结构体和类
例如,类实例(分配内存)调用 swift_allocObject
,后者调用 swift_slowAlloc
,后者调用包含用户探针的 malloc
。
“美化”分配模式
为了使你的火焰图看起来美观(在解构崩溃的堆栈之后),请通过以下方式将以下代码插入到 Linux perf script
代码(上方)中
- 删除
specialized
并将其替换为swift_allocObject
。 - 调用
swift_slowAlloc
,后者调用malloc
。 - 使用
A
表示分配。
这些更改应如下所示
sed -e 's/specialized //g' \
-e 's/;swift_allocObject;swift_slowAlloc;__libc_malloc/;A/g'
为了在分析 Swift 中的内存分配时生成视觉上吸引人的 SVG 文件火焰图,请使用完整命令
perf script | \
/FlameGraph/stackcollapse-perf.pl - | \
swift demangle --simplified | \
sed -e 's/specialized //g' \
-e 's/;swift_allocObject;swift_slowAlloc;__libc_malloc/;A/g' | \
/FlameGraph/flamegraph.pl --countname allocations --flamechart --hash \
> out.svg
克服数据块丢失
将 perf 与 DWARF 调用堆栈展开一起使用时,你可能会遇到此问题
[ perf record: Woken up 189 times to write data ]
Warning:
Processed 4346 events and lost 144 chunks!
Check IO/CPU overload!
[ perf record: Captured and wrote 30.868 MB perf.data (3817 samples) ]
如果 perf
指示它丢失了几个块,则表示它丢失了数据。当 perf
丢失数据时,你可以使用以下选项来帮助解决此问题
- 减少程序执行的工作量。
- 对于每次分配,
perf
都会记录堆栈跟踪。
- 对于每次分配,
- 通过更改
--call-graph dwarf
参数来减少perf
记录的最大堆栈转储。例如,更改为:--call-graph dwarf,2048
- 默认记录最大 4096 字节,呈现深层堆栈。如果不需要高容量输出,可以减少此数字。但是,火焰图可能会显示
[unknown]
堆栈帧,这意味着存在缺失的堆栈帧(以字节为单位)。
- 默认记录最大 4096 字节,呈现深层堆栈。如果不需要高容量输出,可以减少此数字。但是,火焰图可能会显示
- 增加
-m
参数的数量,这是perf
在内存中使用的环形缓冲区的大小,并以PAGE_SIZE
(通常为 4kB)的倍数呈现。 - 将命令
--call-tree dwarf
替换为--call-tree fp
以生成调用树报告,该报告提供程序中函数调用的层次结构视图,显示函数如何被调用以及不同函数之间的关系。
总的来说,这些实践可以帮助你了解程序的行为、识别瓶颈并提高 Swift 应用程序的性能。