🔗 功能:改进字符串内存使用
- 目标:改进短字符串的使用,采用最佳实践、高效、指针安全的 API。
- 状态:通用的代码转换正在进行中
- 预计完成时间:未知
- 开发者:AmosJeffries,FrancescoChemolli
- 更多:https://code.launchpad.net/~kinkie/squid/stringng
🔗 详细信息
改进内存池化字符串的使用和字符串 API。目前的代码在短字符串方面没有采用最佳实践或指针安全。也没有广泛地使用它们来代替可以轻松提供更好内存管理的非池化字符数组。
字符串 API 的计划旨在允许改进对所有当前字符串用法(ESI、ICAP、其他?)的访问,并允许改进和更安全地访问更大的缓冲区(HTTP 解析器、URI 解析器等)。
请阐明您的目标。您试图改进哪些“用法”方面? API 很少是目标,它们通常是实现某些目标(或多个目标)的手段。尚不清楚“改进访问”的含义。
目标:在 Squid 中实现从网络/磁盘读取到网络/磁盘写入的零拷贝数据通路。
更安全的访问,意味着通过代码化的非 char* 访问原始数据缓冲区,我们可以将其存储在状态对象中,并确信指针不会在调用者代码下方失效。当前的 !SquidString/String 实现受限于
- 最大大小 65536 字节。这在早期尝试将其普遍用作 char* 替换时已经很明显。
- 直接访问自控的 char* 缓冲区。不存在非本地缓冲区共享的能力。
- Squid 内现有的用法严格假设以上两个限制始终成立。
FrancescoChemolli 已开始实现样本,借鉴了本页和 AdrianChadd 在 s27_adri 分支中的实现的许多概念。相关通用概念和讨论在 /StringNg 中,一旦更好地勾勒出来,就会合并到这里。
🔗 计划
实现一个单一的半通用引用字符串类,该类同时充当父缓冲区和子字符串(见下文)很容易。一旦创建,我们就可以在 Squid 中逐渐迁移它的使用。如果小心地进行 char* 和 SquidString 的输入/输出转换,我们可以在代码的任何地方无缝插入它们,而不会损失性能,但在两个字符串可以相互引用的地方会有整体提升。
您能否将以上内容重写为一系列具体步骤?这是一个庞大且重要的项目,将影响其他代码。让我们尝试明确步骤。这也有助于您稍后描述当前状态。
步骤
-
讨论下面的设计,直到明确 Squid 的最佳实现是什么。
- JIT、RefCount、const String、其他?
-
创建一个类来实现它。
-
在请求读取处理程序中插入类,并逐步向下推送到请求通路。
-
在回复读取处理程序中插入类,并逐步向下推送到网络回复通路。
-
在存储读取处理程序中插入类,并逐步向下推送到缓存回复通路。
-
查找任何仍在使用 xstrdup() 或等效函数从这些字符串对象创建复制的地方,并逐步向下推送到这些次要通路。
步骤已开始。目前进展如下:
-
Adrian 已以自己的方式为 Squid-2 实现了一个非复制字符串。
-
我们在 Squid-3 的第一步上卡住了。
这与我用于 v6 重做的相同方法。一旦我们通过了第一步,完成应该需要 6-10 个月。
🔗 设计草图
🔗 I/O 缓冲区
更快的字符串计数器依赖于“即时”(Just-In-Time)从缓冲区复制字符串。这样,那些实际上不需要复制的字符串就不会分配内存或执行任何复制。
其第一步是使低级数据缓冲区能够与使用它们的字符串进行通信。
缓冲区需要有一个子字符串列表,通过注册/注销来添加,以及析构函数级联操作。注册/注销是由子字符串在触发 JIT 时启动的。析构函数级联是由缓冲区在需要释放内存时启动的,所有剩余的子字符串必须复制它们的数据,否则就会丢失。
以上设计可行,但还有其他替代方案。您能否将以上设计与一个更简单的设计进行比较,即字符串使用缓冲区时锁定缓冲区,但不直接指向它们;如果字符串需要修改,并且缓冲区有多个锁定,则缓冲区(或其受影响的部分)会复制给该字符串使用,而不会影响其他字符串。
我认为您提到的替代方案是引用计数(RefCounted)的子项,而不是 JIT 复制。这是我最初考虑过的模型之一。它会更简单,更容易使用。Adrian 在将 JIT 模型应用到 Squid-2 时指出的问题是,Squid 中有太多地方假设它接收到的数据将“永远”存在。
选择 RefCounted 选项将以只要还有一个字节的内容被引用锁定,就一直占用缓冲区内存为代价。JIT 方法以消耗内存 + O(1) 的引用计数为代价,换取在父缓冲区中进行链接列表(快速无序 O(4))插入。将子/父缓冲区/字符串设为一种对象类型,可以对这两种方法进行编码和测试,以提高 Squid 中的速度和效益,并保留最佳方案。
请也说明内容插入的开销,或者明确表示我们在此阶段不解决此问题:上述设计优化了解析,但对相反的内容组装过程(例如,构建 HTTP 标头)帮助不大。这个项目是否也应该支持高效、无复制的内容组装?支持这一点的一种方法是支持由多个独立分配区域组成的缓冲区。
JIT 模型的内容插入开销在最坏情况下回退到当前 Squid 复制每个字符串/缓冲区的行为。最佳情况是完全零拷贝。这就是我所说的“无性能损失”的意思。我对客户端和服务器端之间的克隆的当前理解是,它们目前在传输到和离开每个数据通路时都会多次复制相同的数据。
该模型主要希望解决请求/回复解析以及 HTTP 标头部分的传递延迟。数据对象部分预计不会有太大改进。只有 ESI 或其他对象解析可能会受到轻微影响。
注意:一个通用编写的缓冲区本身可能就是一个字符串,它引用另一个更大的缓冲区。
这需要澄清。请定义缓冲区和字符串类的主要角色。例如,字符串负责维护有关缓冲区区域(缓冲区、偏移量、大小)和读/写锁的信息,而缓冲区负责其余所有事情(内存管理、复制、插入、搜索、比较等)。在类特定的部分之前,应该先讨论这种高层角色分离。
子说明是关于:实际上可能不需要这两个类。内存管理器可能完全能够处理分配,将模型的“父”和“子”层留给能够引用其自身类型其他对象的单一类类型。这仍然需要研究当前 MemBuf、char* 和 SquidString 的不同用法和行为。
🔗 JIT 字符串
字符串本身应包含偏移量和对父缓冲区的引用,以及指向父缓冲区的字符指针(如果父级存在),或者指向其自身复制的缓冲区(如果需要)。
我认为您不需要“父缓冲区”和“自己的缓冲区”。一种缓冲区应该足够了。“自己的缓冲区”只是一个当前被一个字符串使用的缓冲区。当字符串或其部分被用户代码复制时,这种情况可能会发生变化。
确实,这就是意图。我使用“父”和“自己”来区分这些对象引用一个单独的“父”对象(具有对外部对象的偏移量+锁的共享缓冲区)或对一个缓冲区拥有主控制权(负责:分配、释放、更改时启动级联通知)的情况。
它们默认应该是只读的,写操作需要复制缓冲区以容纳新内容,而不会修改下方的父缓冲区以影响其他同级字符串。
需要添加一个修改反馈机制,以防止在字符串缓冲区有子字符串时修改它。以防止不必要的复制。
连接(Concatenations)是一个特殊情况,如果字符串已有足够大的复制缓冲区来容纳额外内容,则不需要复制。
🔗 字符串使用者
所有使用这些字符串的代码**不得**为访问或操作引用原始数据缓冲区。应始终提供基于偏移量的 API。
I/O 是一个例外,其中输出可以从当前的字符串缓冲区完成。
最好通过最高级别的构造来完成 I/O,该构造跟踪子字符串,因此是与缓冲区无关的,最重要的是与长度无关。由高级对象决定是输出原始输入缓冲区还是(可能已修改的)子字符串单独重新格式化。
也许我遗漏了什么,但我认为将常量字符串内容指针传递给外部代码(I/O、系统库等)是可以的,只要字符串保证在调用结束前保持不变。传递非常量指针需要写锁定,但在相同条件下也是可以的。这将有助于从现有代码迁移到新 API,并实现代码的更大重用。
常量可以。char* 缓冲区指针不可以。其本质是新的字符串对象不是以 null 结尾的。子对象只有一个指向其他位置原始缓冲区的偏移量和长度,它通过 [] 操作符或查找自身来解引用 char*。许多当前用户依赖于 stdlib 字符串函数的 null 结尾 char*,如果他们尝试随机访问非结尾缓冲区,将会出现严重问题。
🔗 剩余问题
仍然存在缓冲区可能被部分解析为三个部分(或更多,但 3 是最简单的情况)的情况。它维护其头部指针,并且通过引用子项,它可以定位子项的起始偏移量。这允许输出 A 部分。子项本身可以输出 B 部分,无论是否已更改。但是,如果 B 部分子项已更改,我们就没有 C 部分字符串的起始偏移量信息了。这必须在某处干净地维护。
处理此问题的有两种选择:
- 一种小的 Dead-String 类类型,用于映射缓冲区中的这些死空间。当子字符串取消引用自身并指示更改是原因时(其他原因可能是子析构),父项会创建它。
-
我们确保解析器对它们需要处理的整个缓冲区进行解析,用户如果需要的话就保留对 C 部分的引用,并忽略此项,将父缓冲区中所有未引用的区域视为未解析/垃圾。此时所有 I/O **必须**从上到下执行,以包含这些“删除”。
听起来您的缓冲区由多个部分组成。如果是这样,上述设计需要反映这一点。至少,我并不清楚您是提议支持多部分缓冲区,而不是一个缓冲区,有多个字符串指向缓冲区区域。这就是为什么我上面问您是否也要优化内容生成。
我确实没有提议多部分缓冲区。至少不是上面此页面设计中的。对部分的引用仅是对顺序原始缓冲区的逻辑部分,如果部分解析的话。我倾向于忽略这一点,并假设用户会处理任何片段。这更符合此处的解析基础和一般的解析器行为。
这是尝试将一个真实的 N 千字节数据流放入一个缓冲区,并处理其 ASCII 的许多小部分在 Squid 中到处使用的事实。如果不复制它,我们将得到接收一个 triplet 对象 {buf,offset,length} 的函数。或者在预解析标头的情况下,一个由这些对象组成的树,例如,缺少指向 hop-by-hop 标头的那些。然后我们需要直接将它们重构到套接字写入流中(考虑;从网络读取到网络/磁盘写入的非复制)。
尽管如此,输出生成优化确实需要更多思考。
类别:功能
导航:站点搜索,站点页面,类别,🔼 向上