分配

概览

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

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

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

堆栈

一般来说,Swift 有两个用于内存分配的基本位置:

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

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

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

性能分析

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

对于 macOS,您可以使用 Allocations instrumentXcode 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 代码的性能非常有用,原因如下

提示 1:如果您在 Docker 容器中运行 perf,您将需要一个特权容器,以提供必要的权限和访问权限,以便该工具收集性能数据。

提示 2:如果需要 root 访问权限,请在命令前加上 sudo。有关更多信息,请参阅 使 perf 工作

安装 perf 用户探针

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

大多数分配在 Linux 上使用 Swift 程序的 malloc 函数。在分配函数上安装 perf 用户探针可以提供有关何时调用分配函数的信息。

在本例中,为所有分配函数安装了用户探针,因为 Swift 使用其他函数,如 callocposix_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: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 { // 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:* 命令,而不是单独列出每个事件,例如

提示:此方法假定您没有安装其他 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 应用程序的性能。