在进程中断期间使用 python ctypes 时高延迟背后的原因

2023-11-30

在调查 Python 代码库中的关键路径时,我们发现 ctypes 在延迟方面的行为是相当不可预测的。

我们的应用程序的更多背景。我们有很多进程,每个进程都通过共享内存进行通信。我们利用 python 库multiprocessing.RawValue and multiprocessing.RawArray其内部使用ctypes用于数据管理。在生产环境中运行时,我们发现即使是一个简单的get()访问这些共享数据类型大约需要 30-50 us,有时甚至需要 100 us,而且速度相当慢。即使对于蟒蛇来说也是如此。

我创建了这个简单的示例,它创建了一个ctype结构和暴露get() method

import ctypes
import sys
import time
import numpy as np
import random
from decimal import Decimal

def get_time_ns():
    return Decimal(str(time.time_ns()))

class Point(ctypes.Structure):
    _fields_ = [("x", ctypes.c_int),
                ("y", ctypes.c_int)]

    def __init__(self, x, y):
        return super().__init__(x, y)

    def get(self):
        return self.x
        #return str(self.x) + "," + str(self.y)

def benchmark(delay_mode):
    p = Point(10, 20)
    iters = 10
    while iters:
        start_ts = get_time_ns()
        _ = p.get()
        end_ts = get_time_ns()
        print("Time: {} ns".format(end_ts - start_ts))
        iters -= 1
        if delay_mode == 1:
            time.sleep(random.uniform(0, 0.1))

benchmark(int(sys.argv[1]))

当我在非睡眠模式下运行它时,延迟数字如下

[root@centos-s-4vcpu-8gb-fra1-01 experiments]# python3.9 simple_ctype.py 0
Time: 9556 ns
Time: 2246 ns
Time: 1124 ns
Time: 1174 ns
Time: 1091 ns
Time: 1126 ns
Time: 1081 ns
Time: 1066 ns
Time: 1077 ns
Time: 1138 ns

当我在睡眠模式下运行它时,延迟数字如下

[root@centos-s-4vcpu-8gb-fra1-01 experiments]# python3.9 simple_ctype.py 1
Time: 27233 ns
Time: 27592 ns
Time: 31687 ns
Time: 32817 ns
Time: 26234 ns
Time: 32651 ns
Time: 29468 ns
Time: 36981 ns
Time: 31313 ns
Time: 34667 ns

使用理由sleep是模拟我们的生产环境,其中应用程序所做的不仅仅是运行这个循环

有人可以解释一下,与上述热循环相比,当存在中断时,延迟会增加 10 - 20 倍吗?我最好的猜测是 CPU 缓存未命中,但这仍然不能解释这种延迟增加。我对 ctypes 实际如何管理内存也很困惑。难道只是简单的malloc or mmap and malloc。最后但并非最不重要的一点是,如果有人可以帮助我们优化这一点,那就太好了。

系统信息:CentOS 7.9,4核CPU,16 GB RAM。taskset将特定 CPU 核心固定到脚本

仅供参考,我们已经知道 C++/Rust 在这种高精度性能方面比 Python 等高级语言更好,但考虑到时间敏感性和其他业务原因,我们希望在真正遇到语言障碍之前优化 Python 代码的性能


代码睡眠速度变慢的原因有多种。这里,主要有4个原因频率缩放, the TLB/缓存未命中分支未命中。所有这些都是由于上下文切换加上 CPU 长时间不活动造成的。问题是独立于ctypes.


频率缩放

当主流现代处理器没有密集型计算任务时,它自动降低其频率(需经操作系统同意才可配置)。它类似于人类的睡眠:当你无事可做时,你可以睡觉,当你醒来时,需要一些时间才能快速操作(即头晕状态)。对于处理器来说也是同样的事情:处理器从低频(在睡眠调用期间使用)切换到高频(在计算代码期间使用)需要一些时间。 AFAIK,这主要是因为处理器需要调整其电压。这是预期的行为,因为直接切换到最高频率并不节能,因为目标代码可能不会运行很长时间(请参阅滞后现象)和能量消耗一起成长~ frequency**3(由于更高频率所需的电压增加)。

有一种方法可以在 Linux 上轻松检查这一点。您可以使用固定频率并禁用任何类似涡轮的模式。在我的 i5-9600KF 处理器上,我使用了以下行:

echo performance | sudo tee /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor
echo 1 | sudo tee /sys/devices/system/cpu/intel_pstate/no_turbo

您可以使用以下行检查 CPU 的状态:

cat /sys/devices/system/cpu/cpu*/cpufreq/scaling_max_freq
cat /proc/cpuinfo  | grep MHz       # Current frequency for each core

以下是我的机器上更改前后的结果:

# ---------- BEFORE ----------

$ python3 lat-test.py 0
Time: 12387 ns
Time: 2023 ns
Time: 1272 ns
Time: 1096 ns
Time: 1070 ns
Time: 998 ns
Time: 1022 ns
Time: 956 ns
Time: 1002 ns
Time: 1378 ns

$ python3 lat-test.py 1
Time: 6941 ns
Time: 3772 ns
Time: 3544 ns
Time: 9502 ns
Time: 25475 ns
Time: 18734 ns
Time: 23800 ns
Time: 9503 ns
Time: 19520 ns
Time: 17306 ns

# ---------- AFTER ----------

$ python3 lat-test.py 0
Time: 7512 ns
Time: 2262 ns
Time: 1488 ns
Time: 1441 ns
Time: 1413 ns
Time: 1434 ns
Time: 1426 ns
Time: 1424 ns
Time: 1443 ns
Time: 1444 ns

$ python3 lat-test.py 1
Time: 8659 ns
Time: 5133 ns
Time: 3720 ns
Time: 4057 ns
Time: 3888 ns
Time: 4187 ns
Time: 4136 ns
Time: 3922 ns
Time: 4456 ns
Time: 3946 ns

我们可以看到差距明显变小了。此外,结果更加稳定(并且可重复)。请注意,当延迟很小时,性能会降低,因为涡轮增压已被禁用(因此我的处理器不会以其最高可能频率运行)。在我的机器上,最小频率 (0.8 GHz) 和最大频率 (4.6 GHz) 之间的系数为 5.75,这是相当大的,并且证明启用频率缩放(默认)时性能差距的很大一部分是合理的。


有偏差的基准

A 延迟的很大一部分在执行过程中丢失了get_time_ns。这是一个关键点:CPython 是一个缓慢的解释器,因此您无法用它非常精确地测量时间。在我的机器上,CPython 中的空函数调用大约需要 45 ns!表达方式Decimal(str('1676949210508126547'))大约需要 250 纳秒。考虑这一点至关重要,因为您测量的延迟仅比此大 10 倍,而在这种情况下,由于许多开销(包括缓存变冷 - 请参阅下文),此类代码可能会明显变慢。

为了提高基准测试的准确性,我删除了 Decimal 模块的使用以及昂贵且仅使用整数的字符串转换。请注意,即使是基本整数在 CPython 中也远非便宜,因为它们具有可变长度并且是动态分配的,更不用说 CPython 在运行时解释字节码了。一个简单的integer_1 - integer_2在我的机器上大约需要 35 纳秒,而在本机编译代码中则需要不到 1 纳秒。获取函数time_ns来自time模块也大约需要相同的时间,更不用说计时函数本身需要约 50 ns 来执行(1 次获取+执行总共约 85 ns)。

我还将迭代次数从 10 增加到 10_000,以便在接下来的分析部分中效果更加明显。

最终,延迟从 1400/4000 纳秒降至 200/1300 纳秒。这是一个巨大的差异。事实上,200 ns 是如此之小,以至于至少一半的时间仍然丢失了计时开销,并且不是调用p.get()!话虽如此,差距仍然存在。


缓存和 TLB 未命中

剩余开销的一部分是由于高速缓存未命中和 TLB 未命中造成的。事实上,当发生上下文切换时(由于调用sleep), the CPU缓存可以被刷新(不知何故)。事实上,据我所知,它们在上下文切换期间间接刷新主流现代处理器:TLBCPU单元这是一个负责将虚拟内存转换为物理内存的缓存,它被刷新,导致在线程被调度回来时重新加载缓存行。它有一个流程重新安排后对性能产生重大影响因为数据通常需要从慢速 RAM 或至少是较高延迟的缓存(例如 LLC)重新加载。请注意,即使情况并非如此,线程也可以被调度回具有自己的私有 TLB 单元的不同核心上,因此会导致许多缓存未命中。

关于进程之间如何共享内存,您可能还会遇到“TLB 击落”,这也是相当昂贵的。看这个帖子 and this one有关此效果的更多信息。

在Linux上,我们可以使用很棒的perf工具以便跟踪CPU的性能性能事件。以下是 TLB 的两个用例的结果:

# Low latency use-case

            84 429      dTLB-load-misses          #    0,02% of all dTLB cache hits 
       467 241 669      dTLB-loads                                                  
           412 744      dTLB-store-misses                                           
       263 837 789      dTLB-stores                                                    
            47 541      iTLB-load-misses          #   39,53% of all iTLB cache hits 
           120 254      iTLB-loads 
            70 332      mem_inst_retired.stlb_miss_loads                                   
             8 435      mem_inst_retired.stlb_miss_stores       

# High latency use-case

         1 086 543      dTLB-load-misses          #    0,19% of all dTLB cache hits 
       566 052 409      dTLB-loads                                                  
           598 092      dTLB-store-misses                                           
       321 672 512      dTLB-stores            
           834 482      iTLB-load-misses          #  443,76% of all iTLB cache hits 
           188 049      iTLB-loads 
           986 330      mem_inst_retired.stlb_miss_loads                                   
            93 237      mem_inst_retired.stlb_miss_stores 

dTLB 是每核 TLB,用于存储数据页的映射。 sTLB 在内核之间共享。 iTLB 是每个核心的 TLB,用于存储代码页的映射。

我们可以看到 dTLB 加载未命中和 iTLB 加载未命中以及 sTLB 加载/存储的数量大幅增加。这证实了性能问题很可能是由 TLB 未命中引起的。

TLB 未命中会导致更多缓存未命中,从而降低性能。这是我们在实践中可以看到的。事实上,以下是缓存的性能结果:

# Low latency use-case

       463 214 319      mem_load_retired.l1_hit                                     
         4 184 769      mem_load_retired.l1_miss                                    
         2 527 800      mem_load_retired.l2_hit                                     
         1 659 963      mem_load_retired.l2_miss    
         1 568 506      mem_load_retired.l3_hit                                     
            96 549      mem_load_retired.l3_miss    

# High latency use-case

       558 344 514      mem_load_retired.l1_hit                                     
         7 280 721      mem_load_retired.l1_miss                                    
         3 564 001      mem_load_retired.l2_hit                                     
         3 720 610      mem_load_retired.l2_miss         
         3 547 260      mem_load_retired.l3_hit                                     
           105 502      mem_load_retired.l3_miss          

分支未命中

另一部分开销是由于长时间睡眠后条件跳转的预测不太好。这是一个复杂的话题,但人们应该知道,主流现代处理器根据包括过去结果在内的许多参数来预测分支。例如,如果条件始终为真,则处理器可以推测执行条件如果预测实际上是错误的,稍后再恢复它(代价高昂)。现代处理器无法同时预测大量条件跳转:它们为此拥有一个小型缓存,并且随着时间的推移可以快速刷新。问题是 CPython 像大多数解释器一样做了很多条件跳转。因此,上下文切换可能会导致分支跳转缓存的刷新,增加在这种情况下使用的条件跳转的开销,从而导致更高的延迟。

以下是我机器上的实验结果:

# Low latency use-case

       350 582 368      branch-instructions                                         
         4 629 149      branch-misses             #    1,32% of all branches        

# High latency use-case

       421 207 541      branch-instructions                                         
         8 392 124      branch-misses             #    1,99% of all branches     

请注意,在我的机器上,分支未命中应该需要大约 14 个周期。这意味着 14 ms 的间隙,因此每次迭代约 1400 ns。话虽如此,在两次函数调用之间只测量了一小部分时间get_time_ns().

有关此主题的更多信息,请阅读这个帖子.

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

在进程中断期间使用 python ctypes 时高延迟背后的原因 的相关文章

随机推荐

  • 使用 find() Motor 时发生 BadYieldError [MongoDB + Tornado]

    我是 python 龙卷风框架的新手 我在 MongoDB 中有一小部分数据 我在 python 文件中使用一个简单的 get 函数 我得到一个BadYieldError当使用db collection find 选项 但db collec
  • C# 中大于/小于号(例如 Action>)意味着什么?

    example public event Action
  • 如何在 Presto 中将日、月、年字段合并为日期?

    我的表中的条目与日期一起保存为不同的字段day month and year 我想将日期读取为日期类型 正确的做法是什么 其他方式 date format d d d 2020 3 31 基于在 Presto 上计算日期和周末日期
  • Windows 多处理

    我发现 Windows 在多处理方面有点笨拙 我对此有疑问 pydoc 指出你应该使用多处理时保护 Windows 应用程序的入口点 这是否意味着仅创建新进程的代码 例如 Script 1 import multiprocessing de
  • 重定向在 WordPress 中不起作用?

    我正在使用下面的代码在 WordPress 中进行重定向 currentPage explode SERVER REQUEST URI current page url currentPage 0 if current page url e
  • 查找 _id 在 mongodb 中不起作用

    我有一个名为 Releases 的集合 其中包含一个名为 Product 的子文档 我的收藏看起来像这样 id ObjectId 5b1010e4ef2afa6e5edea0c2 version abc description p abc
  • 当新电子邮件到达共享邮箱中的任何子文件夹时运行代码

    我想在任何新电子邮件到达特定共享邮箱时运行代码 当电子邮件到达 INBOX 文件夹时触发该事件 如果新电子邮件直接进入其子文件夹 则该事件不会触发 例如 电子邮件受保护 收件箱 子文件夹1 如果收件箱中的任何子文件夹收到新电子邮件 我应该更
  • 赌场 24/7 员工名册的数据库模型

    我们目前使用基于笔 纸的名册来管理赌场的桌面游戏工作人员 每行代表一名员工 每列代表 20 分钟的时间段 每个单元格代表员工被分配到的表 或者他们被分配到休息时间 员工轮班的开始和结束时间各不相同 他们可以处理的游戏 技能也各不相同 我们需
  • 编译并运行C++代码运行时

    有谁知道如何在程序运行时编译您编写的 C 代码 后来我想运行该代码 我想这样做是因为我正在尝试制作一款可以教您编程的游戏 因此用户必须在游戏运行时编写代码并进行测试 谢谢你的帮助 如果您选择一种在设计时考虑到嵌入的语言 例如 LUA 或 p
  • 如何在 Prolog 中将谓词作为另一个谓词的参数传递?

    我有这 3 个谓词 times X Y Result is X Y minus X Y Result is X Y plus X Y Result is X Y 例如我想通过times 2 2 in the plus X Y 像这样plus
  • JVM 是开源代码吗?

    JVM 是开源代码吗 如果没有 如何获取JVM的代码 这完全取决于您使用哪个 JVM 如果您使用 OpenJDK JVM 那么您可以从以下位置获取源代码here or here来自 OpenJDK 项目列表 或者here来自 GitHub
  • 无法连接到SQL Server 2008?

    我一直在尝试各种技巧 从启用 TCP IP 向防火墙添加例外到戳比尔盖茨巫毒娃娃 但似乎没有什么对我有用 通过management studio连接到sql server 2008真的那么难吗 也许 抢劫银行会更容易 当我弄清楚要抢劫哪家银
  • RSA 加密的结果是否保证是随机的

    我使用 RSACryptoServiceProvider 来加密一些小数据块 对于我正在研究的解决方案 重要的是 如果使用相同的公钥对同一段源数据加密两次 则结果 加密的数据块 不相同 我已经用一个例子对此进行了检查 它的效果正如我所希望的
  • 获取发件人电子邮件地址

    我有以下 VBA 代码 用于在收到特定主题时自动发送电子邮件 Private WithEvents Items As Outlook Items Private Sub Application Startup Dim objNS As Ou
  • 我们如何按类指定 jQuery 验证插件的规则?

    The jQuery 验证插件效果很好并且非常容易使用 selector validate 只需设置 CSS 类 如 必需的电子邮件 就会显示默认消息 但是 我需要自定义消息 该文档说您可以使用元素的键值对及其相应的消息来指定规则 sele
  • 如何在 Pygame 中使用 Sprite Collide

    我正在制作一个非常简单的游戏 小鸟 玩家 必须躲避岩石 如果它被岩石击中 你就会失败 我正在尝试使用 pygame sprite collide rect 来判断它们是否接触 但我似乎不知道如何正确使用它 这是我的代码 import pyg
  • Android - 创建进度对话框

    我是安卓开发新手 我想开发一个dialog with a progressbar在我的应用程序中 当我点击搜索按钮时dialog应与出现progressbar 显示在切换到另一个之前进度正在进行中activity 请用示例代码建议我 Use
  • 使用 Visual Studio 2008 编译 php 扩展,MODULE ID 与 php 不匹配

    使用 VC9 2008 和 VC10 2010 编译我自己的 php 扩展后 使用以下步骤 http blog slickedit com 2007 09 creating a php 5 extension with visual c 2
  • 使用低级键盘钩子更改键盘字符

    我正在创建自定义键盘布局 作为开始步骤 我想让用户按下一个键 让我的键盘钩子拦截它 并输出我选择的不同键 我找到了这个键盘挂钩代码 我试图根据我的目的对其进行稍微修改 http blogs msdn com toub archive 200
  • 在进程中断期间使用 python ctypes 时高延迟背后的原因

    在调查 Python 代码库中的关键路径时 我们发现 ctypes 在延迟方面的行为是相当不可预测的 我们的应用程序的更多背景 我们有很多进程 每个进程都通过共享内存进行通信 我们利用 python 库multiprocessing Raw