docs: 讲清"值传递的开销只在 New 这一次", 并补上浅拷贝/深拷贝的分岔
两处之前没说清楚的地方。
一、值传递的代价只发生在构造那一次, 不影响之后的操作
原来只列了 New(值) 3227ns vs New(&v) 25ns, 容易被读成"传值之后一直慢"。
实际上构造完成后两者的内部表示完全相同(都是类型描述符 + 地址), 后续
Get/Set 走同一条代码路径:
构造后 Get 构造后 Set
New(v) 传值构造 49.7ns / 2 allocs 27.2ns / 0 allocs
New(&v) 传指针构造 50.3ns / 2 allocs 27.1ns / 0 allocs
差异在噪声范围内。所以要看的是**构造频次**而不是访问频次: 构造一次访问很多次
的话那点开销会被摊薄; 每个请求都构造、对象又含 map/slice 时才值得计较。
二、流程图补上值分支内部的二次分岔
图是在 needsClone 那次优化之前画的, 一直写着"传值 = DeepClone", 已经不准:
不含引用成分(纯值 struct/数组/字符串) -> 逐字节浅拷贝, 约 85 ns
字符串虽然内部有指针, 但底层数组不可变, 共享是安全的
含指针/切片/map/interface(递归包含字段与数组元素) -> DeepClone
这也是"能不能 copy 一份再取指针"的答案: 纯值类型可以(库里已经这么做了),
含引用的类型不行 —— 浅拷贝只复制 slice/map 的头部, 底层数据仍与调用方共享,
写入会穿透, 那就不再是隔离副本了。
顺带把代价数字更正为实测值(原来写的 1518ns/44allocs 是另一个更小的样本)。
This commit is contained in:
@@ -107,7 +107,18 @@ rfx.Set("Name", "Bob")
|
|||||||
| 50 元素 slice | 594 ns / 6 allocs | 25 ns / 1 alloc | **24x** |
|
| 50 元素 slice | 594 ns / 6 allocs | 25 ns / 1 alloc | **24x** |
|
||||||
| 8 字段纯值 struct | 85.5 ns / 2 allocs | 25.5 ns / 1 alloc | 3.4x |
|
| 8 字段纯值 struct | 85.5 ns / 2 allocs | 25.5 ns / 1 alloc | 3.4x |
|
||||||
|
|
||||||
判断标准就一句:**这份数据我会不会通过 R 去写?写了穿透出去有没有问题?**
|
**注意这笔开销只发生在 `New` 这一次。** 构造完成后两者的内部表示完全相同,
|
||||||
|
后续 `Get`/`Set` 走同一条代码路径,速度没有差别:
|
||||||
|
|
||||||
|
| | 构造后 `Get` | 构造后 `Set` |
|
||||||
|
|---|---|---|
|
||||||
|
| `New(v)` 传值构造 | 49.7 ns / 2 allocs | 27.2 ns / 0 allocs |
|
||||||
|
| `New(&v)` 传指针构造 | 50.3 ns / 2 allocs | 27.1 ns / 0 allocs |
|
||||||
|
|
||||||
|
所以要看的是**构造频次**而不是访问频次:构造一次访问很多次的话,
|
||||||
|
传值那点开销会被摊薄;每个请求都构造、对象又含 map/slice 时才值得计较。
|
||||||
|
|
||||||
|
至于选哪个,标准就一句:**这份数据我会不会通过 R 去写?写了穿透出去有没有问题?**
|
||||||
|
|
||||||
- 只读,或者本来就想改调用方的数据 → **传指针**
|
- 只读,或者本来就想改调用方的数据 → **传指针**
|
||||||
- 需要一份互不干扰的副本 → 传值(或者传指针之后用 `Scope()` 取局部副本)
|
- 需要一份互不干扰的副本 → 传值(或者传指针之后用 `Scope()` 取局部副本)
|
||||||
|
|||||||
+15
-10
@@ -1,5 +1,5 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 960 742" role="img"
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 960 742" role="img"
|
||||||
aria-label="New 的流程:先按具体类型走快速分支(nil 和 valuex.Nil 返回 Nil、R 原样返回、[]R 直接包装),否则经 normalizeInputValue 解包、递归解引用指针和 interface、校验目标类型,再按 isPtr 分岔:传指针则直接借用,传值则 DeepClone 深拷贝,最后构造 rfx">
|
aria-label="New 的流程:先按具体类型走快速分支(nil 和 valuex.Nil 返回 Nil、R 原样返回、[]R 直接包装),否则经 normalizeInputValue 解包、递归解引用指针和 interface、校验目标类型,再按 isPtr 分岔:传指针则直接借用,传值则再按类型是否含引用成分分两条:纯值逐字节浅拷贝,含 map/slice/指针才 DeepClone 递归,最后构造 rfx">
|
||||||
<style>
|
<style>
|
||||||
/* 独立 SVG 通过 img 引用时是隔离渲染的: 拿不到页面的 CSS 变量, 也继承不到
|
/* 独立 SVG 通过 img 引用时是隔离渲染的: 拿不到页面的 CSS 变量, 也继承不到
|
||||||
currentColor。所以配色和明暗适配都必须写在文件内部。
|
currentColor。所以配色和明暗适配都必须写在文件内部。
|
||||||
@@ -99,8 +99,8 @@
|
|||||||
<path d="M 230 510 L 230 528 L 150 528 L 150 556" fill="none" stroke="var(--accent)" stroke-width="1.5" marker-end="url(#n-ah-fast)"/>
|
<path d="M 230 510 L 230 528 L 150 528 L 150 556" fill="none" stroke="var(--accent)" stroke-width="1.5" marker-end="url(#n-ah-fast)"/>
|
||||||
<text class="e-label" x="140" y="547" text-anchor="end" fill="var(--accent)">是 · 传的是指针</text>
|
<text class="e-label" x="140" y="547" text-anchor="end" fill="var(--accent)">是 · 传的是指针</text>
|
||||||
|
|
||||||
<path d="M 230 510 L 230 528 L 560 528 L 560 556" fill="none" stroke="var(--fallback)" stroke-width="1.5" marker-end="url(#n-ah-slow)"/>
|
<path d="M 230 510 L 230 528 L 631 528 L 631 556" fill="none" stroke="var(--fallback)" stroke-width="1.5" marker-end="url(#n-ah-slow)"/>
|
||||||
<text class="e-label" x="572" y="547" fill="var(--fallback)">否 · 传的是值</text>
|
<text class="e-label" x="643" y="547" fill="var(--fallback)">否 · 传的是值</text>
|
||||||
|
|
||||||
<!-- R7a borrow -->
|
<!-- R7a borrow -->
|
||||||
<rect x="30" y="556" width="240" height="88" rx="5" fill="var(--accent-soft)" stroke="var(--accent)" stroke-width="1.5"/>
|
<rect x="30" y="556" width="240" height="88" rx="5" fill="var(--accent-soft)" stroke="var(--accent)" stroke-width="1.5"/>
|
||||||
@@ -109,16 +109,21 @@
|
|||||||
<text class="n-sub" x="150" y="616" text-anchor="middle" fill="var(--fg)">写入直接反映到原数据</text>
|
<text class="n-sub" x="150" y="616" text-anchor="middle" fill="var(--fg)">写入直接反映到原数据</text>
|
||||||
<text class="n-sub" x="150" y="634" text-anchor="middle" fill="var(--muted)">指针模式</text>
|
<text class="n-sub" x="150" y="634" text-anchor="middle" fill="var(--muted)">指针模式</text>
|
||||||
|
|
||||||
<!-- R7b deep clone -->
|
<!-- R7b 值模式: 再按 needsClone 分两条 -->
|
||||||
<rect x="400" y="556" width="320" height="88" rx="5" fill="var(--fallback-soft)" stroke="var(--fallback)" stroke-width="1.5"/>
|
<rect x="330" y="556" width="602" height="88" rx="5" fill="none" stroke="var(--line)" stroke-width="1.5" stroke-dasharray="5 4"/>
|
||||||
<text class="n-label" x="560" y="578" text-anchor="middle" fill="var(--fallback)">DeepClone 深拷贝</text>
|
<text class="grp" x="344" y="574" fill="var(--faint)">cloneForValueInput · 看类型描述符的 needsClone</text>
|
||||||
<text class="n-sub" x="560" y="598" text-anchor="middle" fill="var(--fg)">递归复制整个对象图,返回新指针</text>
|
|
||||||
<text class="n-sub" x="560" y="616" text-anchor="middle" fill="var(--fg)">写入不影响原数据 · 值模式</text>
|
<rect x="344" y="582" width="278" height="50" rx="5" fill="var(--accent-soft)" stroke="var(--accent)" stroke-width="1.5"/>
|
||||||
<text class="n-sub" x="560" y="634" text-anchor="middle" fill="var(--muted)">比借用贵得多,且循环引用会栈溢出</text>
|
<text class="n-sub" x="483" y="600" text-anchor="middle" fill="var(--accent)">不含引用成分 → 逐字节浅拷贝</text>
|
||||||
|
<text class="n-sub" x="483" y="618" text-anchor="middle" fill="var(--fg)">纯值 struct / 数组 · 约 85 ns</text>
|
||||||
|
|
||||||
|
<rect x="638" y="582" width="278" height="50" rx="5" fill="var(--fallback-soft)" stroke="var(--fallback)" stroke-width="1.5"/>
|
||||||
|
<text class="n-sub" x="777" y="600" text-anchor="middle" fill="var(--fallback)">含引用成分 → DeepClone 递归</text>
|
||||||
|
<text class="n-sub" x="777" y="618" text-anchor="middle" fill="var(--fg)">含 map/slice/指针 · 10 键 map 约 3200 ns</text>
|
||||||
|
|
||||||
<!-- converge -->
|
<!-- converge -->
|
||||||
<path d="M 150 644 L 150 672 L 230 672" fill="none" stroke="var(--fg)" stroke-width="1.5"/>
|
<path d="M 150 644 L 150 672 L 230 672" fill="none" stroke="var(--fg)" stroke-width="1.5"/>
|
||||||
<path d="M 560 644 L 560 672 L 230 672" fill="none" stroke="var(--fg)" stroke-width="1.5"/>
|
<path d="M 631 644 L 631 672 L 230 672" fill="none" stroke="var(--fg)" stroke-width="1.5"/>
|
||||||
<line x1="230" y1="672" x2="230" y2="686" stroke="var(--fg)" stroke-width="1.5" marker-end="url(#n-ah)"/>
|
<line x1="230" y1="672" x2="230" y2="686" stroke="var(--fg)" stroke-width="1.5" marker-end="url(#n-ah)"/>
|
||||||
|
|
||||||
<!-- R8 newRfx -->
|
<!-- R8 newRfx -->
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 9.7 KiB After Width: | Height: | Size: 10 KiB |
+28
-2
@@ -49,9 +49,35 @@
|
|||||||
| | 做法 | 代价 | 语义 |
|
| | 做法 | 代价 | 语义 |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| **传指针** `New(&v)` | 直接借用该指针 | 约 25 ns,零拷贝 | 写入**直接反映到原数据** |
|
| **传指针** `New(&v)` | 直接借用该指针 | 约 25 ns,零拷贝 | 写入**直接反映到原数据** |
|
||||||
| **传值** `New(v)` | `DeepClone` 递归复制整个对象图 | **约 1518 ns / 44 allocs**(10 元素 + map 的对象) | 写入**不影响原数据** |
|
| **传值 + 纯值类型** | 逐字节浅拷贝 | 约 85 ns | 写入**不影响原数据** |
|
||||||
|
| **传值 + 含引用成分** | `DeepClone` 递归复制整个对象图 | 10 键嵌套 map 约 **3227 ns / 119 allocs** | 写入**不影响原数据** |
|
||||||
|
|
||||||
**传值比传指针贵约 60 倍**,而且随对象规模增长。如果不需要副本语义,一律传指针。
|
传值这一支还要再分一次,依据是类型描述符里预先算好的 `needsClone`
|
||||||
|
(构建期算一次,不在热路径):
|
||||||
|
|
||||||
|
- **不含引用成分**(纯值 struct、数组、字符串)→ 逐字节复制就已经完全独立了。
|
||||||
|
字符串虽然内部有指针,但底层数组不可变,共享是安全的。
|
||||||
|
- **含指针 / 切片 / map / interface**(递归包含字段与数组元素)→ 必须 `DeepClone`。
|
||||||
|
只做浅拷贝的话,slice 和 map 复制的只是**头部**,底层数据仍与调用方共享,
|
||||||
|
写入会穿透过去 —— 这正是"能不能只 copy 一份再取指针"这个问题的答案:
|
||||||
|
对纯值类型可以(库里已经这么做了),对含引用的类型不行。
|
||||||
|
|
||||||
|
**这笔开销只发生在 `New` 这一次,不影响之后的任何操作。**
|
||||||
|
构造完成后两者的内部表示完全相同(都是"类型描述符 + 地址"),
|
||||||
|
后续 `Get`/`Set` 走的是同一条代码路径:
|
||||||
|
|
||||||
|
| | 构造后 `Get` | 构造后 `Set` |
|
||||||
|
|---|---|---|
|
||||||
|
| `New(v)` 传值构造 | 49.7 ns / 2 allocs | 27.2 ns / 0 allocs |
|
||||||
|
| `New(&v)` 传指针构造 | 50.3 ns / 2 allocs | 27.1 ns / 0 allocs |
|
||||||
|
|
||||||
|
所以判断标准是**构造频次**,不是访问频次:
|
||||||
|
|
||||||
|
- 构造一次、访问很多次 → 传值的那点开销会被摊薄,可以忽略
|
||||||
|
- 每个请求都构造(比如把查询结果包成 R)、而且对象含 map/slice → 这时候
|
||||||
|
那 3227 ns 是实打实的,值得改成传指针
|
||||||
|
|
||||||
|
不需要副本语义就传指针;取不到地址的表达式先落一个局部变量。
|
||||||
|
|
||||||
`DeepClone` 还有一个已知限制:**遇到循环引用会栈溢出**(`CloneValue` 没有已访问集合)。
|
`DeepClone` 还有一个已知限制:**遇到循环引用会栈溢出**(`CloneValue` 没有已访问集合)。
|
||||||
这是旧实现就有的行为,当前实现直接复用了它,所以两边表现一致 ——
|
这是旧实现就有的行为,当前实现直接复用了它,所以两边表现一致 ——
|
||||||
|
|||||||
Reference in New Issue
Block a user