程序分析-动态程序切片

news2025/1/17 9:31:41

最近在调研符号执行工具优化方式时,发现好几篇工作都用到了动态程序切片,以前大部分接触的都是静态切片,对动态切片几乎不了解。所以开始学习动态切片,我主要参考的是90年的一篇上古paper。

1.静态依赖图和静态切片

说到程序切片,就离不开程序依赖图这个概念,我之前写了一篇blog介绍静态程序依赖图,这里再简单提一下。

在程序依赖图中:

  • 每个结点表示一个语句(statement,与之相对的是块语句-compound statement)或者指令(read, write, assignment)。

  • 边包括控制依赖边和数据依赖边(paper中给出的依赖方向和我平时应用的相反,这里按照paper中的来)

    • 数据依赖边 v i → v j v_i \rightarrow v_j vivj,意味着语句 v i v_i vi 使用了变量 var,是在语句 v j v_j vj 中定义的。并且从 v j v_j vj v i v_i vi 的路径上 var 没有重新被定义。

    • 控制依赖边 v i → v j v_i \rightarrow v_j vivj 表示 v i v_i vi 是否被执行取决于条件表达式 v j v_j vj 的值。

以下面程序为例

示例1

x = read(); // S1
if (x < 0) { // S2
    y = f1(x); // S3
    z = g1(x); // S4
}
else {
    if (x == 0) { // S5
        y = f2(x); // S6
        z = g2(x); // S7
    }
    else { 
        y = f3(x); // S8
        z = g3(x); // S9
    }
}
write(y); // S10
write(z); // S11

静态依赖图如下(标注了 CD 的是控制依赖边):

CD
CD
CD
CD
CD
CD
CD
S1
S2
S3
S4
S5
S6
S7
S8
S9
S10
S11

在静态依赖图中,在 S10 关于变量 y 的reaching-definition包括了 S3, S6, S8,在整个依赖图上进行切片,可以得到slice为 {1, 2, 3, 5, 6, 8}

2.动态切片

上面示例中的程序,S3, S6, S8 都会对 y 赋值,对于任意输入 x,3条语句只有1个会执行。所以动态切片指挥保留1个。

如果输入 x = -1,那么最终的动态slice为 {1, 2, 3},显然动态slice比静态slice要更精简,更容易定位bug。

下面部分我就给出paper中提到的4种进行动态切片的算法,首先给出execution history(执行历史)的概念。

execution history是一个在给定testcase下的语句序列 { v 1 , . . . , v n } \{v_1, ..., v_n\} {v1,...,vn},序列按照执行顺序排序好。如果同一个语句在execution history中出现多次,那么会用上标表示,比如下面示例:

示例2

n = read(); // S1
z = 0; // S2
y = 0; // S3
i = 1; // S4
while (i <= n) { // S5
    z = f1(z, y); // S6
    y = f2(y); // S7
    i = i + 1; // S8
}
write(z); // S9

依赖图为

CD
CD
CD
S1
S2
S3
S4
S5
S6
S7
S8
S9

上面示例当输入 n = 2 时,execution history为 { 1 , 2 , 3 , 4 , 5 1 , 6 1 , 7 1 , 8 1 , 5 2 , 6 2 , 7 2 , 8 2 , 5 3 , 9 } \{1, 2, 3, 4, 5^1, 6^1, 7^1, 8^1, 5^2, 6^2, 7^2, 8^2, 5^3, 9\} {1,2,3,4,51,61,71,81,52,62,72,82,53,9}

给定程序 P 的execution history hist,testcase test,变量 var。关于 var 的动态slice是 hist 中执行结果对 var 有影响的结点集合。简单的说,动态slice仅关注 hist 中的语句而不是整个程序 P 中的。

2.1.方法1

在第一个示例中 S10y 的值受 S3, S6, S8 影响。但这3个语句一次只可能执行1个。那么在静态依赖图中标注出当前testcase执行过的语句,并且切片时只遍历标注的结点。那么就可以获得当前testcase的动态切片。

跟纯静态切片相比,这种方法多出的步骤就是标记执行过的语句,并且切片时只遍历标记过的语句。

以第一个示例为例,输入 x = -1。execution history为 1, 2, 3, 4, 10, 11。所以对于变量 y slice出的结果是 1, 2, 3

但是这种朴素的方法也不是总会产生精准的动态切片,它有时候会引入额外的结点(过拟合)。示例2中当输入 n = 1 时,execution history为 { 1 , 2 , 3 , 4 , 5 1 , 6 , 7 , 8 , 5 2 , 9 } \{1, 2, 3, 4, 5^1, 6, 7, 8, 5^2, 9\} {1,2,3,4,51,6,7,8,52,9},此时关于变量 z 产生的动态切片为包含所有结点。但实际上execution history中 7 7 7y 进行了赋值而之后再也没使用,所以 S7 不应该出现在动态切片中。

可以看到上面的问题是在循环语句中引入的,那么从静态slice -> 方法1,由分支语句引起的依赖过拟合问题已得到缓解。

2.2.方法2

方法2可以看作方法1的改进。方法1的问题在于,一个语句可能在程序流图中具有同一变量的多个到达定义(Reaching-Definition),但同一时刻只有1个会影响其变量值。

在示例2中, S6 关于变量 y 依赖于 S3S7,关于变量 z 依赖于 S2S6。当 n = 1 时,这4个依赖语句都被执行了,但是 S6 --> S7 之间的依赖关系并没有被触发,方法1错误的包含了这种依赖关系。

方法2的思路就是标记边(方法1是标记结点),在程序执行的过程中如果一条依赖边边被触发了,那么标记该依赖边。切片时只遍历标记过的依赖边。

以示例2为例,输入 n = 1 时,依赖边 S6 --> S7S9 --> S2 没有被标记,那么最终动态切片为 {1, 2, 3, 4, 5, 6, 8}(没有7)。

当没有循环出现的时候,上面的方法能找到准确的动态切片。但是有时候,方法2依旧会引入一些不必要的语句。以下面示例为例

示例3

n = read(); // S1
i = 1; // S2
while (i <= n) { // S3
    x = read(); // S4
    if (x < 0) // S5
        y = f1(x); // S6
    else
        y = f2(x); // S7
    z = f3(y); // S8
    write(z); // S9
    i = i + 1; // S10
}

当输入 n = 2,2次输入 x 分别为 -4, 3 时。execution history为 { 1 , 2 , 3 1 , 4 1 , 5 1 , 6 , 8 1 , 9 1 , 1 0 1 , 3 2 , 4 2 , 5 2 , 7 , 8 2 , 9 2 } \{1, 2, 3^1, 4^1, 5^1, 6, 8^1, 9^1, 10^1, 3^2, 4^2, 5^2, 7, 8^2, 9^2\} {1,2,31,41,51,6,81,91,101,32,42,52,7,82,92},在第2轮, 9 2 → 7 9^2 \rightarrow 7 927,在第1轮, 9 1 → 6 9^1 \rightarrow 6 916。当要对变量 z 进行动态切片时,采用方法2会将 S6, S7 都包括进来,而实际上只需要 S7 就够了。因为第2轮 9 2 → 7 9^2 \rightarrow 7 927

一种朴素的解决方案在标记一个语句的依赖关系前取消其已有的标记(unmark),这种方案对示例3是管用的,但是依旧可能会造成错误的切片,以下面示例为例

示例4

n = read(); // S1
a = 0; // S2
i = 1; // S3
while (i <= n) { // S4
    x = read(); // S5
    if (x < 0) // S6
        y = f1(x); // S7
    else
        y = f2(x); // S8
    z = f3(y); // S9
    if (z > 0) // S10
        a = f4(a, z); // S11
    i = i + 1; // S12
}
write(a); // S13

循环执行了2次,第一次执行了 S7S11,第二次执行了 S8 跳过了 S11,那么在结尾对 a 进行切片时,slice中出现的是 S8 而不是 S7。根源在于 S7 --> S9 的依赖关系被 S8 --> S9 取代了,而 a 依赖的是第一次循环的结果。

2.3.方法3

方法2中因为循环的问题,1个语句可能在execution history中出现多次,每次可能会依赖不同的语句,从而导致过拟合。这就催生出了第3种方法:

对于execution history中的语句,如果出现多次,那么每一次都在动态依赖图上创建一个不同的结点,它们只依赖于当前对它们有影响的结点。在这种情况下,每一个结点针对一个变量只会依赖于一个结点。

以示例3为例,输入 n = 3,3次 x 取值分别为 -4, 3, 2, execution history为 { 1 , 2 , 3 1 , 4 1 , 5 1 , 6 1 , 8 1 , 9 1 , 1 0 1 , 3 2 , 4 2 , 5 2 , 7 , 8 2 , 9 2 , 1 0 2 , 3 3 , 4 3 , 5 3 , 6 2 , 8 3 , 9 3 , 1 0 3 , 3 4 } \{1, 2, 3^1, 4^1, 5^1, 6^1, 8^1, 9^1, 10^1, 3^2, 4^2, 5^2, 7, 8^2, 9^2, 10^2, 3^3, 4^3, 5^3, 6^2, 8^3, 9^3, 10^3, 3^4\} {1,2,31,41,51,61,81,91,101,32,42,52,7,82,92,102,33,43,53,62,83,93,103,34}

依赖图如下图所示,同一条语句循环n次就会创建n个结点,每次的依赖关系可能不同,对同一个变量只会依赖1个结点。图中粗体显示的为对 write(z); 进行slice得到的结点集合。

在这里插入图片描述

2.4.方法4

方法3中动态依赖图的大小(节点和边的总数)通常是无限的。这是因为图中的节点数等于execution history中的语句数而非代码的语句数,而这通常取决于运行时输入的值。而同时,每个程序只能有有限数量的可能动态切片——每个切片都是(有限)程序的子集。

这表明限制动态依赖图中的节点数量是可行的。第4个方法便是利用这个insight:不要为execution history中的每一个语句创建一个新节点,而是仅当具有相同传递依赖关系的另一个节点不存在时才创建一个节点。

基于上述方法创建出来的依赖图叫Reduced Dynamic Dependence Graph(简化动态依赖图,RDDG)。为了创建RDDG,执行时需要创建2个表 DefnNode, PredNode

  • DefnNode 将1个变量名映射到上次赋值到这个变量中的依赖图结点,在C++中可通过 map 实现,在python中通过 dict 实现。

  • PredNode 将1个control predicate statement(条件判断语句,基本上就是 if, while 条件)映射为该predicate在execution history中的最后一次出现相对应的结点

此外,依赖图中每一个结点,都会包含1个 set 型成员变量:ReachableStmts,包含该结点依赖路径上的全部语句(是语句不是结点,不区分第几次执行)。算法执行过程包括:

  • 1.每当一个语句 S i S_i Si 被执行,算法会计算由最近一次对 S i S_i Si 引用的变量赋值的结点 组成的集合 D D D。以及由 S i S_i Si 控制依赖的结点(结点对应条件判断在execution history中最近一次出现)组成的集合 C C C

  • 2.如果 S i S_i Si

    • 2.1.之前已经执行过并且对应的结点是 n n n,其直接后代与 D ∪ C D \cup C DC 相同,则 S i S_i Si 这次执行对应的结点依旧是 n n n

    • 2.2.否则,为 S i S_i Si 创建一个新的结点 n ′ n^{'} n n ′ n^{'} n所有的出边指向 D ∪ C D \cup C DC 中的所有结点。

  • 3.如果 S i S_i Si 对一些变量进行了赋值,那么将这些变量在 DefnNode 中的表项更新为 n ′ n^{'} n

  • 4.如果 S i S_i Si 是一个control predicate(条件判断),PredNode S i S_i Si 对应的表项更新为 n ′ n^{'} n

上述4个步骤在没有环路依赖的情况下可以正常工作,但是程序中存在环路依赖(由循环语句导致的),上述依赖图缩减工作不能正常运行。可以添加下面步骤解决:

  • 对于语句 S i S_i Si,在需要创建结点的时候(2.2)首先确定对应结点 n ′ n^{'} n 的直接后继结点集合( D ∪ C D \cup C DC 中的)中的任意一结点,记作 v v v n ′ n^{'} n 依赖于 v v v),是否对 S i S_i Si 之前执行对应的结点产生依赖以及 n ′ n^{'} n 的其它直接后继结点是否从 v v v 可达。这可以通过检查 n ′ n^{'} nReachableStmts 是否是 v v vReachableStmts 的子集来实现。

    • 如果是,那么将 n ′ n^{'} n v v v 合并。

示例3对应的RDDG如下,每个结点都标注了其 ReachableStmts,当 S3 第2次执行时,对应结点 3 2 3^2 32 依赖于 10 10 10。并且 3 1 3^1 31ReachableStmts: {1, 2, 3} 10 10 10ReachableStmts: {1, 2, 3, 10} 的子集。因此 3 2 3^2 32 并入 10 10 10

在进行slice时,直接查找结点就能找到slice包含的语句,都不需要遍历RDDG了。

在这里插入图片描述
用方法4分析示例3大致过程如下:

  • 执行代码 n = read();,且该语句并没有依赖任何结点,所以 D = ∅ , C = ∅ D = \emptyset, C = \empty D=,C=,该语句此前未执行过,因此创建结点 n 1 n_1 n1。该语句定义了变量 nDefnNode[n] = n1

  • 执行代码 i = 1;,跟 S1 一样,没有执行过没有依赖结点,创建依赖图结点 n 2 n_2 n2。定义了变量 iDefnNode[i] = n2

  • 执行代码 i <= n,引用了变量 i, n ,上一次赋值分别在 n 1 , n 2 n_1, n_2 n1,n2 结点(查 DefnNode 表),没有控制依赖于其它结点。 D = { n 1 , n 2 } , C = ∅ D = \{n_1, n_2\}, C = \empty D={n1,n2},C=,没有执行过,创建结点 n 3 n_3 n3。并且添加 n 3 → n 2 , n 3 → n 1 n_3 \rightarrow n_2, n_3 \rightarrow n_1 n3n2,n3n1 的数据依赖边。S3 为控制语句,PredNode[S3] = n3

  • 执行代码 x = read();,对 S3 有控制依赖,上一次执行是在 n 3 n_3 n3(查 PredNode 表)。 D = ∅ , C = { n 3 } D = \empty, C = \{n_3\} D=,C={n3},没有执行过,创建结点 n 4 1 n_4^1 n41。添加控制依赖边 n 4 1 → n 3 n_4^1 \rightarrow n_3 n41n3。定义了变量 xDefnNode[x] = n41

  • 执行代码 x < 0,引用变量 x,上一次赋值是在 n 4 1 n_4^1 n41;控制依赖于 S3,上一次执行是在 n 3 n_3 n3 D = { n 4 1 } , C = { n 3 } D = \{n_4^1\}, C = \{n_3\} D={n41},C={n3};没有执行过,创建结点 n 5 1 n_5^1 n51。添加数据依赖边 n 5 1 → n 4 1 n^1_5 \rightarrow n_4^1 n51n41,控制依赖边 n 5 1 → n 3 n^1_5 \rightarrow n_3 n51n3。为控制语句,PredNode[S5] = n51

  • 执行代码 y = f1(x);,引用了变量 x,上一次赋值是在 n 4 1 n_4^1 n41;控制依赖于 S5,上一次执行是在 n 5 1 n_5^1 n51 D = { n 4 1 } , C = { n 5 1 } D = \{n_4^1\}, C = \{n_5^1\} D={n41},C={n51};没有执行过,创建结点 n 6 1 n_6^1 n61;添加数据依赖边 n 6 1 → n 4 1 n^1_6 \rightarrow n_4^1 n61n41,控制依赖边 n 6 1 → n 5 1 n^1_6 \rightarrow n^1_5 n61n51S6 定义了 yDefnNode[y] = n61

  • 执行代码 z = f3(y);,引用了变量 y,上一次赋值是在 n 6 1 n_6^1 n61;控制依赖于 S3,上一次执行是在 n 3 n_3 n3 D = { n 6 1 } , C = { n 3 } D = \{n_6^1\}, C = \{n_3\} D={n61},C={n3};没有执行过,创建结点 n 8 1 n_8^1 n81;添加数据依赖边 n 8 1 → n 6 1 n^1_8 \rightarrow n_6^1 n81n61,控制依赖边 n 8 1 → n 3 n^1_8 \rightarrow n_3 n81n3S8 定义了 zDefnNode[z] = n81

  • 执行 write(z); … ,创建 n 9 1 n_9^1 n91,添加数据依赖边 n 9 1 → n 8 1 n_9^1 \rightarrow n_8^1 n91n81,控制依赖边 n 9 1 → n 3 n_9^1 \rightarrow n_3 n91n3

  • 执行 i = i + 1,创建 n 10 n_{10} n10,添加数据依赖边 n 10 → n 2 n_{10} \rightarrow n_2 n10n2,控制依赖边 n 1 0 → n 3 n_10 \rightarrow n_3 n10n3DefnNode[i] = n10

  • 再次执行 i <= n,引用变量 i, n,上次赋值分别是在 n 10 , n 1 n_{10}, n_1 n10,n1 D = { n 10 , n 2 } , C = ∅ D = \{n_{10}, n_2\}, C = \empty D={n10,n2},C=,执行过一次,上次执行结点为 n 3 n_3 n3,但是 n 3 n_3 n3 后继为 n 1 , n 2 n_1, n_2 n1,n2。所以新建结点 n 3 ′ n_3^{'} n3,应该添加数据依赖边 n 3 ′ → n 10 , n 3 ′ → n 1 n_3^{'} \rightarrow n_{10}, n_3^{'} \rightarrow n_1 n3n10,n3n1。但是 n 3 ′ n_3^{'} n3 对应的 ReachableStmts1, 2, 3, 10,跟 n 10 n_{10} n10 相同,算子集,因此 n 3 ′ n_3^{'} n3 并入 n 10 n_{10} n10 n 3 ′ → n 1 n_3^{'} \rightarrow n_1 n3n1 变成 n 10 → n 1 n_{10} \rightarrow n_1 n10n1PredNode[S3] = n10

  • 再次执行 x = read();,控制依赖于 S3 D = ∅ , C = { n 10 } D = \empty, C = \{n_{10}\} D=,C={n10}。执行过一次但是 n 4 1 n_4^1 n41 的直接后继为 n 3 n_3 n3,与 D ∪ C D \cup C DC 不同。新建 n 4 2 n_4^2 n42,且不需要合并结点。DefnNode[x] = n42

  • 再次执行 x < 0,引用 x,控制依赖于 S3 D = n 4 2 , C = n 10 D = n_4^2, C = n_{10} D=n42,C=n10。执行过一次但是依旧需要新建结点,新建 n 5 2 n_5^2 n52,不需要合并结点。PredNode[S5] = n52

  • 执行 y = f2(x);,引用 x,控制依赖于 S5 D = { n 4 2 } , C = { n 5 2 } D = \{n_4^2\}, C = \{n_5^2\} D={n42},C={n52}。没有执行过,新建 n 7 n_7 n7。添加数据依赖边 n 7 → n 4 2 n_7 \rightarrow n_4^2 n7n42,控制依赖边 n 7 → n 5 2 n_7 \rightarrow n_5^2 n7n52DefnNode[y] = n7

  • 再次执行 z = f3(x);,新建结点 n 8 2 n_8^2 n82 D = { n 7 } , C = { n 10 } D = \{n_7\}, C = \{n_{10}\} D={n7},C={n10}。添加数据依赖边 n 8 2 → n 7 n_8^2 \rightarrow n_7 n82n7,控制依赖边 n 8 2 → n 10 n_8^2 \rightarrow n_{10} n82n10DefnNode[z] = n82

  • 再次执行 write(z),新建 n 9 2 n_9^2 n92,添加数据依赖边 n 9 2 → n 8 2 n_9^2 \rightarrow n_8^2 n92n82,控制依赖边 n 9 2 → n 10 n_9^2 \rightarrow n_{10} n92n10

  • 再次执行 i = i + 1,引用 i,控制依赖于 S3 D = { n 10 } , C = { n 10 } D = \{n_{10}\}, C = \{n_{10}\} D={n10},C={n10}。可以新建结点 n 10 ′ n_{10}^{'} n10,但是 n 10 ′ n_{10}^{'} n10 n 10 n_{10} n10ReachableStmts 相同,因此可以合并2个结点。

  • 第3次执行 i <= n,引用 i, n D = { n 10 , n 1 } D = \{n_{10}, n_1\} D={n10,n1}。同样,新建了结点但是又并入 n 10 n_{10} n10

  • 第3次执行 x = read(); D = ∅ , C = { n 10 } D = \empty, C = \{n_{10}\} D=,C={n10}。之前执行过对应结点为 n 4 2 n_4^2 n42,后继为 { n 10 } \{n_{10}\} {n10} D ∪ C D \cup C DC 相同。因此不新建结点。

  • 第3次执行 x < 0,同理不新建结点。

  • 再次执行 y = f1(x);,引用变量 x,控制依赖于 S5 D = { n 4 2 } , C = { n 5 2 } D = \{n_4^2\}, C = \{n_5^2\} D={n42},C={n52}。执行过对应结点 n 6 1 n^1_6 n61,后继和 D ∪ C D \cup C DC 不同,新建 n 6 2 n_6^2 n62。添加数据依赖边 n 6 2 → n 4 2 n_6^2 \rightarrow n_4^2 n62n42,控制依赖边 n 6 2 → n 5 2 n_6^2 \rightarrow n_5^2 n62n52DefnNode[y] = n62

  • 对于之后2个语句,它们之前执行过但是由于之前对应结点后继和 D ∪ C D \cup C DC,不同,需要新建结点。

  • 对于最后执行的 i = i + 1,依旧是先新建结点然后合并。

3.总结

这篇blog介绍了4种动态切片的方法,后一种是前一种的改版。动态切片的目的就根据执行过程简化静态切片。方法1和方法2都算是在静态依赖分析的基础上小修小补实现,方法3相比方法2不需要先静态分析提高了精度但是代价太高,方法4是方法3的优化版。

和静态分析相比,方法4,方法3纯粹通过遍历execution history实现动态数据依赖分析,但是依旧需要先进行静态控制依赖分析,而静态数据依赖分析(Reaching-Definition,到达定值分析)需要遍历CFG(ICFG)实现,复杂度更高。

4.参考文献

Hiralal Agrawal and Joseph R. Horgan. 1990. Dynamic program slicing. SIGPLAN Not. 25, 6 (Jun. 1990), 246–256. https://doi.org/10.1145/93548.93576

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/161893.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Obsidian Templater 批量新建笔记

Obsidian Templater 批量新建笔记 背景 工作中的新项目都有这么几个固定的笔记&#xff0c;比如项目背景、原理图设计、PCB设计、调试等&#xff0c;每个笔记文件又有一些固定的内容&#xff0c;想着在开始一个新项目时&#xff0c;一次性把这些笔记都建好&#xff0c;并且统…

一文教会你 如何在Github中创建仓库?如何将多个项目放到一个仓库中管理?如何将本地项目上传到GitHub中?

文章目录前言1、如何在GitHub中创建仓库1.1 点击New repository1.2 填写仓库的基本信息1.3 完成创建2 、克隆仓库地址到本地2.1 克隆地址2.2 克隆到本地2.3 将后端代码项目提交到远程服务器2.4 将前端代码项目提交到远程服务器3、Github上查看自己上传项目代码4、使用这个方式存…

【程序分析】Code Lifting

我理解&#xff0c;Code Lifting和反编译的概念类似 常规的编译过程是这样的&#xff1a; 这是一个从高级形式到低级形式的过程&#xff0c;一般叫做 lowering 而反编译&#xff0c;是这样一个过程&#xff1a; 这个过程是完全相反的&#xff0c;叫做lifting 需要注意 Bi…

FPGA:逻辑函数的卡诺图化简法

文章目录最小项与最小项表达式最小项的定义最小项的性质逻辑函数的最小项表达式卡诺图化简法用卡诺图表示逻辑函数卡诺图的引出两变量卡诺图三变量卡诺图四变量卡诺图已知逻辑函数真值表&#xff0c;画卡诺图已知逻辑函数画卡诺图用卡诺图化简逻辑函数化简的依据化简的步骤用卡…

Hexo + Butterfly 侧边栏公众号

原文链接 &#xff1a;Hexo Butterfly 侧边栏公众号 推荐阅读 基于 Hexo 从零开始搭建个人博客&#xff08;一&#xff09;: 环境准备基于 Hexo 从零开始搭建个人博客&#xff08;二&#xff09;: 项目初识基于 Hexo 从零开始搭建个人博客&#xff08;三&#xff09;: 主题安…

Lichee_RV学习系列--stream移植

Lichee_RV学习系列文章目录 Lichee_RV学习系列—认识Lichee Rv Dock、环境搭建和编译第一个程序 Lichee_RV学习系列—移植dhrystone 文章目录Lichee_RV学习系列文章目录一、stream简介二、源码下载三、文件移植1、makefile文件编译makefile文件移植四、运行结果五、移植过程中…

【尚硅谷】Java数据结构与算法笔记08 - 查找算法

文章目录一、查找算法介绍二、线性查找算法三、二分查找算法3.1 思路分析3.2 代码实现四、插值查找算法4.1 思路分析4.2 代码实现4.3 注意事项五、斐波那契&#xff08;黄金分割法&#xff09;查找算法5.1 思路分析5.2 原理讲解5.3 代码实现一、查找算法介绍 在 java 中, 我们…

C++多线程入门及基础知识

什么是C多线程 线程即操作系统进行CPU任务调度的最小单位。C的多线程并发&#xff0c;简单理解的话就是&#xff0c;将任务的不同功能交由多个函数实现&#xff0c;创建多个线程&#xff0c;每个线程执行一个函数&#xff0c;一个任务就同时由不同线程执行。 什么时候使用多…

微信小程序:骨架屏的实现方法

骨架屏是为了展示一个页面骨架而不含有实际的页面内容&#xff0c;从渲染效率上来讲&#xff0c;骨架屏它并不能使首屏渲染加快。由于骨架屏的一些使用又向用户渲染了额外的一些内容&#xff0c;这些内容是额外添加的、本来是不需要渲染的&#xff0c;它反而从整体上加长了首屏…

Windows 虚拟磁盘驱动开发(采用原始办法实现类似Storport框架的相同功能)

其实以前讲述windows平台下的磁盘驱动的开发挺多&#xff0c;而且时间也是非常早。以下连接&#xff1a;https://blog.csdn.net/fanxiushu/article/details/9903123?spm1001.2014.3001.5501https://blog.csdn.net/fanxiushu/article/details/11713357?spm1001.2014.3001.5501…

游戏开发 状态同步

【状态同步】1、将所有的操作发送给Server&#xff08;T1&#xff09;&#xff0c;由Server计算&#xff08;T2&#xff09;&#xff0c;并返回结果&#xff08;T3&#xff09;。权威服务器架构能够防止很多的作弊&#xff0c;但是直接用这种方法会让游戏的响应变得迟缓。如果 …

three games 之 桌球

接下来介绍一些 Vue4 中的一些进阶使用&#xff0c;希望对大家有所帮助&#xff0c;谢谢。 如果文中有不对、疑惑的地方&#xff0c;欢迎在评论区留言指正&#x1f33b; 一、项目结构 Vuex 并不限制你的代码结构。但是&#xff0c;它规定了一些需要遵守的规则&#xff1a; …

移动硬盘怎么分区?

硬盘分区指的是硬盘上被划分出来的区块&#xff0c;可用于分类存储各种数据。而我们日常购买的移动硬盘通常来说分为两种&#xff0c;一种是买回来已分好区的&#xff0c;还有一种是未经过分区的。如果移动硬盘没有经过分区&#xff0c;那么在将它连接到电脑的USB接口时&#x…

android四大组件之四-BroadCast实现原理分析

前言&#xff1a; 一开始的目标是解决各种各样的ANR问题的&#xff0c;但是我们知道&#xff0c;ANR总体上分有四种类型&#xff0c;这四种ANR类型有三种是和四大组件相对应的&#xff0c;所以&#xff0c;如果想了解ANR发生的根因&#xff0c;对安卓四大组件的实现原理必须要…

伙伴云与飞书、金山办公一同入选亿欧2022中国数字化企业服务商TOP50

近日&#xff0c;由中关村国家自主创新示范区展示中心、中关村会展与服务产业联盟与亿欧联合举办的SHOWTECH2022-WIM 创新者年会”在京顺利召开&#xff0c;会上&#xff0c;亿欧网重磅发布《2022世界创新奖榜单》。伙伴云凭借10年来为企业数字化转型赋能的成功经验和卓越贡献&…

数据结构学习-队列

坚持看完&#xff0c;结尾有思维导图总结 这里写目录标题队列的定义于性质如何实现队列的功能初始化队列入队列出队列队列的销毁队列取队头数据队列取队尾数据判断队列是否为空判断队列长度总结队列的定义于性质 队列是一种数据结构&#xff0c;他储存数据的方式就和排队一样 …

二十六、Kubernetes中Horizontal Pod Autoscaler(HPA)控制器详解

1、概述 在kubernetes中&#xff0c;有很多类型的pod控制器&#xff0c;每种都有自己的适合的场景&#xff0c;常见的有下面这些&#xff1a; ReplicationController&#xff1a;比较原始的pod控制器&#xff0c;已经被废弃&#xff0c;由ReplicaSet替代 ReplicaSet&#xff…

年终盘点(三)丨2022计讯物联团队不负韶华,奋力前行

光阴荏苒&#xff0c;时光悄然&#xff0c;成长的齿轮不断转动。2022年&#xff0c;计讯人在挑战中创造不凡&#xff0c;2023年&#xff0c;计讯人在希望中迎接新未来。 回首过去&#xff0c;计讯物联团队不断壮大&#xff0c;在奋勇前行中以坚持书写拼搏&#xff0c;在知难而…

记好这24个ES6方法,用于解决实际开发的JS问题

本文主要介绍24中es6方法&#xff0c;这些方法都挺实用的&#xff0c;本本请记好&#xff0c;时不时翻出来看看。 1.如何隐藏所有指定的元素 1 const hide (el) > Array.from(el).forEach(e > (e.style.display none)); 2 3 // 事例:隐藏页面上所有<img>元素? …

echarts——实现 面积图+柱状图+折线图等——基础积累

因为到年底了&#xff0c;很多项目组都开始做年终汇报&#xff0c;年终汇报的展示形式最常见的就是看板。 样式美观&#xff0c;可以放到电视机或者大屏上&#xff0c;通过图表的形式进行展示&#xff0c;简单明了&#xff0c;通俗易懂。 直接上最终效果图&#xff1a;是一个…