内存分配

概述

在服务端 Swift 应用程序中,内存分配对于创建对象、操作数据结构和管理资源等各种任务至关重要。Swift 根据需要分配内存资源,并提供了内置的内存管理机制,例如自动引用计数(ARC),来处理分配、释放和内存所有权。

内存分配通过分配每个对象或数据结构所需的精确内存量来帮助优化内存使用,从而减少内存浪费并提高应用程序性能。但是,Swift 内存分配可以增加填充,以强制执行需要由硬件有效访问的数据类型或结构的内存对齐要求,从而降低内存访问问题未对齐的风险并提高性能。

此外,适当的分配管理可以防止内存泄漏,并确保内存在不再需要时被释放。这有助于维护服务端应用程序的稳定性和可靠性。

堆和栈

通常,Swift 有两个基本的内存分配位置:

Swift 自动在堆或栈数据结构中分配内存。

对于 Swift 中的高性能软件,理解你的堆内存分配来源并减少软件的内存分配数量至关重要。识别这些问题与识别其他性能问题类似,例如:

注意:虽然堆内存分配在计算开销方面可能相对昂贵,但它们提供了灵活性和动态内存管理能力,对于处理可变大小或动态数据结构等任务至关重要。

分析

根据项目的具体需求,你可以使用不同的工具和技术来分析你的 Swift 代码。一些常用的分析技术包括:

对于 macOS,你可以使用 Xcode Instruments 中的 Allocations instrument 来帮助你分析和优化应用程序中的内存使用。Allocations instrument 跟踪所有堆和匿名虚拟内存分配的大小和数量,并按类别组织它们。

如果你的生产工作不是在 macOS 上,而是在 Linux 上运行,那么分配数量可能会根据你的设置有很大的不同。

本文档主要关注堆内存分配的数量,而不是它们的大小。

开始使用

Swift 的优化器在 release 模式下生成更快的代码,并分配更少的内存。通过在 release 模式下分析你的 Swift 代码并根据结果进行优化,你可以在应用程序中获得更好的性能和效率。

按照以下步骤操作:

步骤 1. 构建你的代码release 模式下运行以下命令:

swift run -c release

步骤 2. 安装 perf 来分析你的代码,通过收集到的性能相关数据,优化你的 Swift 服务端应用程序的性能。

步骤 3. 克隆 FlameGraph 项目 生成可视化火焰图,帮助你快速识别代码库中的热点,可视化调用路径,理解执行流程,并优化性能。要生成火焰图,你需要在你的机器或容器中克隆 FlameGraph 仓库到 ~/FlameGraph 目录。

运行以下命令把 https://github.com/brendangregg/FlameGraph 仓库克隆岛 ~/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 代码很有价值,原因如下:

提示 1:如果你在 Docker 容器中运行 perf,你将需要一个有特权的容器,以提供工具收集性能数据所需的必要权限和访问。 提示 2:如果你需 root 访问权限,请在命令前加上 sudo。更多相关信息请参阅 Getting perf to work

安装 perf 用户探针

如前所述,本文档的示例程序侧重于内存分配的数量

在 Linux 上,大多数内存分配使用 Swift 中的 malloc 函数。在内存分配函数上安装 perf 用户探针可以提供内存分配函数被调用时的信息。

In this instance,a user probe was installed for all allocation functions because Swift uses other functions like calloc and posix_memalign. 在这个例子中,需要这也写才能为所有内存分配函数安装用户探针,因为 Swift 使用了其他函数,如 callocposix_memalign

# 确定 libc 的路径
libc_path=$(readlink -e /lib64/libc.so.6 /lib/x86_64-linux-gnu/libc.so.6)

# 删除 libc 上所有现有的用户探针(你可以用 * 来代替,也可以单独列出它们)
perf probe --del 'probe_libc:*'

# 给 `malloc`,`calloc`,和 `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:mallocprobe_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 { // 为了更短的栈
                    doRemainingRequests(remaining, overallResult: overallResult, eventLoop: eventLoop)
                }
            }.whenFailure { error in
                overallResult.fail(error)
            }
        } else {
            return overallResult.succeed(())
        }
    }

    let promise = eventLoop.makePromise(of: Void.self)
    // 发起请求
    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:* 命令,而不是单独列出每个事件,例如:

提示:这种方法假定您没有安装其他 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

以下是这个命令结构的分解:

您的程序输出应该类似于这样:

<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

以下是这个命令结构的分解:

命令完成后,将生成一个 SVG 文件,您可以在浏览器中打开该文件。

注意:根据数据大小、算法复杂性、资源限制(如 CPU 功率或内存)、代码优化不良或效率低下、外部服务、API 或网络延迟,可能会导致运行时间很长。

阅读火焰图

这个火焰图是本节中示例程序的直接结果。将鼠标悬停在堆栈帧上以获取更多信息,或点击任何堆栈帧可放大子树。

Flame graph

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 聚合对等内存分配函数的调用次数:

注意:在 Apple 平台上,Swift 使用的分配函数数量略多于 Linux。

步骤 2. 数据收集完成后,运行此命令创建 SVG 文件:

cat raw.stacks |\
    /FlameGraph/stackcollapse.pl - | \
    swift demangle --simplified | \
    /FlameGraph/flamegraph.pl --countname allocations \

        --width 1600 > out.svg

您会注意到这个命令与 perf 调用类似,但有以下不同:

其他 perf 技巧

Swift 的内存分配模式

根据火焰图提供的信息优化内存分配并提高代码效率,可以帮助您的 Swift 代码变得更高效和视觉上更具吸引力。Swift 中的内存分配形式会根据分配的内存类型和使用方式而有所不同。

Swift 中一些常见的内存分配形式包括:

例如,一个类实例(进行内存分配)会调用 swift_allocObject,该方法调用 swift_slowAlloc,然后调用包含用户探针的 malloc

“美化”内存分配模式

为了让您的火焰图看起来更美观(在解开堆栈混淆后),请通过以下方式将这段代码插入到 Linux perf script 代码中(如上所述):

这些更改应该像这样:

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 丢失数据时,您可以使用以下选项来帮助解决问题:

总体来说,这些做法帮助您理解程序的行为,识别瓶颈,并提高您的 Swift 应用程序的性能。