Skip to content

Commit 2e62877

Browse files
da-liiiclaude
andauthored
[1228] raw_insert/raw_remove 逐元素搬移改为 memmove 块移动 (#4398)
Co-authored-by: Claude <noreply@anthropic.com>
1 parent 9472285 commit 2e62877

5 files changed

Lines changed: 245 additions & 8 deletions

File tree

devel/1228.md

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,3 +106,48 @@ xmake b --yes tree_helper_bench && xmake r tree_helper_bench
106106
- `moebius/Data/Tree/tree_cursor.cpp`:新增 `label_input`/`label_math`
107107
缓存,valid_cursor/pre_correct 调用点替换
108108
- `devel/1228.md`:本节记录
109+
110+
## 第八项:raw_insert/raw_remove 块移动
111+
112+
### What
113+
114+
`tree_observer.cpp``raw_insert` / `raw_remove`(每次编辑最终落到的原语,
115+
apply 调度链的终点)原先用逐元素赋值搬移孩子,每个被移动孩子一次
116+
引用计数加+减。改为 memmove 块移动:
117+
118+
- 插入:resize 腾位后 memmove 尾部上移;洞内 stale 位用 placement-new
119+
默认句柄覆盖(不动计数),再由赋值语句正常接管。
120+
- 删除:先把被删孩子拷入临时数组(+1 接管所有权),memmove 尾部下移,
121+
resize 收缩,临时数组析构时统一释放被删孩子的所有权。
122+
123+
### Why
124+
125+
在宽容器(大文档/表格/长 concat)前部插入或删除时,原实现是
126+
O(n) 次引用计数操作,memmove 后为纯内存搬运。表格编辑、文档开头输入
127+
等场景直接受益。
128+
129+
### 记账分析(关键正确性依据)
130+
131+
数组对每个槽位恰好持有一份所有权;memmove 位级搬移使所有权随位移动
132+
转移,计数不变;插入洞与被删元素按上述方式显式补齐加减,账目平衡。
133+
`tm_delete_array` 逐槽析构、`tm_resize_array` 增长时构造新槽,均与该账目
134+
兼容(容量桶内增删不触发 realloc,槽内容始终为有效句柄或 stale 位)。
135+
136+
### 测量(参考分支,1000 段文档头部插入+删除 ×1000)
137+
138+
12.03 ms → 0.56 ms(约 21×)。
139+
140+
### 涉及文件
141+
142+
- `moebius/Data/Tree/tree_observer.cpp``tree_observer.hpp`
143+
(补 raw_insert/raw_remove 声明与 Doxygen 注释)
144+
- `moebius/bench/Data/Tree/tree_observer_bench.cpp`(新增)
145+
- `tests/Data/tree_observer_test.cpp`(新增:位置正确性、
146+
共享子树引用计数、跨容量桶插删)
147+
148+
### 本机构建 / 测试
149+
150+
`xmake b --yes libmoebius` 构建通过,`xmake test "moebius_tests/*"` 17/17
151+
通过。`tree_observer_test` 本机因 Qt 安装缺 `Qt6Bodymovin.lib` 无法链接
152+
(存量问题,`tree_helper_test` 同样失败),测试与 bench 以参考分支
153+
CI 为准。

moebius/Data/Tree/tree_observer.cpp

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
#include "tree_cursor.hpp"
77
#include "tree_helper.hpp"
88

9+
#include <string.h>
10+
911
extern tree the_et;
1012

1113
/******************************************************************************
@@ -248,12 +250,18 @@ raw_insert (tree& ref, int pos, tree t) {
248250
ref->label=
249251
ref->label (0, pos) * t->label * ref->label (pos, N (ref->label));
250252
else {
251-
int i, n= N (ref), nr= N (t);
253+
int n= N (ref), nr= N (t);
254+
// 块移动代替逐元素赋值:每个被移动孩子的引用计数加减一次都省去。
255+
// 记账:memmove 把 [pos, n) 的句柄位原样搬到 [pos+nr, n+nr),
256+
// 数组对其所有权引用随位移动转移;洞里的 stale 位用 placement-new
257+
// 默认句柄覆盖(不减计数),随后被赋值语句正常接管
252258
AR (ref)->resize (n + nr);
253-
for (i= n - 1; i >= pos; i--)
254-
ref[i + nr]= ref[i];
255-
for (i= 0; i < nr; i++)
256-
ref[pos + i]= t[i];
259+
tree* a= A (AR (ref));
260+
memmove (a + pos + nr, a + pos, (size_t) (n - pos) * sizeof (tree));
261+
for (int i= 0; i < nr; i++)
262+
new ((void*) (a + pos + i)) tree ();
263+
for (int i= 0; i < nr; i++)
264+
a[pos + i]= t[i];
257265
}
258266
if (!is_nil (ref->data)) {
259267
ref->data->notify_insert (ref, pos, is_atomic (t) ? N (t->label) : N (t));
@@ -287,9 +295,13 @@ raw_remove (tree& ref, int pos, int nr) {
287295
if (is_atomic (ref))
288296
ref->label= ref->label (0, pos) * ref->label (pos + nr, N (ref->label));
289297
else {
290-
int i, n= N (ref) - nr;
291-
for (i= pos; i < n; i++)
292-
ref[i]= ref[i + nr];
298+
int n= N (ref) - nr;
299+
// 先把被删孩子拷出(引用计数 +1,接管数组的所有权),memmove 尾部
300+
// 下移(计数随位移动转移,超出新长度的重复位直接丢弃),
301+
// 最后 tmp 析构时统一释放被删孩子的所有权
302+
array<tree> tmp (A (AR (ref)) + pos, nr);
303+
tree* a= A (AR (ref));
304+
memmove (a + pos, a + pos + nr, (size_t) (n - pos) * sizeof (tree));
293305
AR (ref)->resize (n);
294306
}
295307
if (!is_nil (ref->data)) ref->data->done (ref, mod);

moebius/Data/Tree/tree_observer.hpp

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,17 @@ void remove_node (path p);
2020
void set_cursor (path p, tree data);
2121
void touch (path p);
2222

23+
/**
24+
* @brief 在 ref 的 pos 处直接插入 t 的孩子,绕过 apply 的调度与观察者协议
25+
* @note 仅由 raw_apply 及测试/基准使用
26+
*/
27+
void raw_insert (tree& ref, int pos, tree t);
28+
/**
29+
* @brief 在 ref 的 pos 处直接删除 nr 个孩子,绕过 apply 的调度与观察者协议
30+
* @note 仅由 raw_apply 及测试/基准使用
31+
*/
32+
void raw_remove (tree& ref, int pos, int nr);
33+
2334
void assign (tree& ref, tree t);
2435
void insert (tree& ref, int pos, tree t);
2536
void remove (tree& ref, int pos, int nr);
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/** \file tree_observer_bench.cpp
2+
* \copyright GPLv3
3+
* \details Benchmark for raw tree modifications on the editing path
4+
* \date 2026
5+
*/
6+
7+
#include "nanobench.h"
8+
#include "observers.hpp"
9+
#include "tree_helper.hpp"
10+
#include "tree_observer.hpp"
11+
12+
#include <moebius/drd/drd_std.hpp>
13+
14+
using namespace moebius;
15+
16+
// 修改链引用的全局编辑树与 ip 观察者定义在 mogan 主程序侧,
17+
// 基准中给出未挂接的独立桩实现
18+
tree the_et;
19+
path
20+
obtain_ip (tree& ref) {
21+
(void) ref; // 未挂接的树没有 ip
22+
return path ();
23+
}
24+
bool
25+
ip_attached (path ip) {
26+
(void) ip;
27+
return false;
28+
}
29+
observer
30+
list_observer (observer o1, observer o2) {
31+
(void) o1;
32+
(void) o2; // 基准树未挂观察者,不会被调用
33+
return observer ();
34+
}
35+
36+
/** 构造有 1000 个孩子的宽文档 */
37+
static tree
38+
mk_wide_doc () {
39+
tree doc (DOCUMENT);
40+
for (int i= 0; i < 1000; i++)
41+
doc << tree ("para" * as_string (i));
42+
return doc;
43+
}
44+
45+
int
46+
main () {
47+
ankerl::nanobench::Bench bench;
48+
bench.minEpochIterations (10).unit ("op");
49+
50+
// 单个 op 内插删配平,避免 epoch 叠加导致文档规模无界增长
51+
tree doc= mk_wide_doc ();
52+
bench.run ("raw_insert+remove head x1000", [&] {
53+
for (int i= 0; i < 1000; i++)
54+
raw_insert (doc, 0, tree (DOCUMENT, tree ("x")));
55+
for (int i= 0; i < 1000; i++)
56+
raw_remove (doc, 0, 1);
57+
});
58+
return 0;
59+
}

tests/Data/tree_observer_test.cpp

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
/******************************************************************************
2+
* MODULE : tree_observer_test.cpp
3+
* DESCRIPTION: Tests for raw tree modifications and refcount integrity
4+
* COPYRIGHT : (C) 2026 Mogan developers
5+
*******************************************************************************
6+
* This software falls under the GNU general public license version 3 or later.
7+
* It comes WITHOUT ANY WARRANTY WHATSOEVER. For details, see the file LICENSE
8+
* in the root directory or <http://www.gnu.org/licenses/gpl-3.0.html>.
9+
******************************************************************************/
10+
11+
#include <QtTest/QtTest>
12+
13+
#include "base.hpp"
14+
#include "tree_helper.hpp"
15+
#include "tree_observer.hpp"
16+
17+
using namespace moebius;
18+
19+
class TestTreeObserver : public QObject {
20+
Q_OBJECT
21+
22+
private slots:
23+
void init () { init_lolly (); }
24+
25+
void test_raw_insert ();
26+
void test_raw_remove ();
27+
void test_insert_remove_refcount ();
28+
};
29+
30+
void
31+
TestTreeObserver::test_raw_insert () {
32+
tree doc (DOCUMENT, tree ("a"), tree ("b"), tree ("d"));
33+
// 在头部插入
34+
raw_insert (doc, 0, tree (DOCUMENT, tree ("x")));
35+
QVERIFY (N (doc) == 4);
36+
QVERIFY (doc[0] == tree ("x"));
37+
QVERIFY (doc[1] == tree ("a"));
38+
// 在中部插入多个
39+
raw_insert (doc, 2, tree (DOCUMENT, tree ("m"), tree ("n")));
40+
QVERIFY (N (doc) == 6);
41+
QVERIFY (doc[1] == tree ("a"));
42+
QVERIFY (doc[2] == tree ("m"));
43+
QVERIFY (doc[3] == tree ("n"));
44+
QVERIFY (doc[4] == tree ("b"));
45+
QVERIFY (doc[5] == tree ("d"));
46+
// 在尾部插入
47+
raw_insert (doc, 6, tree (DOCUMENT, tree ("z")));
48+
QVERIFY (N (doc) == 7);
49+
QVERIFY (doc[6] == tree ("z"));
50+
}
51+
52+
void
53+
TestTreeObserver::test_raw_remove () {
54+
tree doc (DOCUMENT, tree ("a"), tree ("b"), tree ("c"), tree ("d"),
55+
tree ("e"));
56+
// 头部删除
57+
raw_remove (doc, 0, 1);
58+
QVERIFY (N (doc) == 4);
59+
QVERIFY (doc[0] == tree ("b"));
60+
// 中部删除多个
61+
raw_remove (doc, 1, 2);
62+
QVERIFY (N (doc) == 2);
63+
QVERIFY (doc[0] == tree ("b"));
64+
QVERIFY (doc[1] == tree ("e"));
65+
// 尾部删除
66+
raw_remove (doc, 1, 1);
67+
QVERIFY (N (doc) == 1);
68+
QVERIFY (doc[0] == tree ("b"));
69+
}
70+
71+
void
72+
TestTreeObserver::test_insert_remove_refcount () {
73+
// 交叉插入/删除并共享子树:若数组块移动的引用计数记账有误,
74+
// 后续拷贝比较或进程退出时会崩溃或得到错误内容
75+
tree shared (CONCAT, tree ("s1"), tree ("s2"));
76+
tree doc (DOCUMENT);
77+
for (int round= 0; round < 50; round++) {
78+
tree ins (DOCUMENT);
79+
ins << shared << tree ("t") << shared;
80+
raw_insert (doc, 0, ins);
81+
QVERIFY (doc[0] == shared);
82+
QVERIFY (doc[2] == shared);
83+
if (round % 2 == 0) raw_remove (doc, 0, 1);
84+
}
85+
QVERIFY (N (doc) == 125);
86+
// 大数组跨容量桶的插入/删除
87+
tree big (DOCUMENT);
88+
for (int i= 0; i < 1000; i++)
89+
big << tree ("p" * as_string (i));
90+
for (int i= 0; i < 40; i++)
91+
raw_insert (big, 500, tree (DOCUMENT, tree ("q")));
92+
QVERIFY (N (big) == 1040);
93+
QVERIFY (big[500] == tree ("q"));
94+
QVERIFY (big[499] == tree ("p499"));
95+
QVERIFY (big[539] == tree ("q"));
96+
QVERIFY (big[540] == tree ("p500"));
97+
raw_remove (big, 500, 40);
98+
QVERIFY (N (big) == 1000);
99+
QVERIFY (big[500] == tree ("p500"));
100+
}
101+
102+
#ifdef QTTEXMACS
103+
QTEST_MAIN (TestTreeObserver)
104+
#else
105+
int
106+
main () {
107+
return 0;
108+
}
109+
#endif
110+
#include "tree_observer_test.moc"

0 commit comments

Comments
 (0)