【问题标题】:DFA state transition table compressionDFA 状态转换表压缩
【发布时间】:2015-03-19 07:28:07
【问题描述】:

我想第一次写编译器,我的参考是“编译器原理、技术和工具”。对于词法设计,我编写了语言标记的 FA,现在我想从状态转换表中编写 C# 代码,但它是一个 40 X 30 矩阵,该矩阵中只有 50 个条目!我想压缩这个稀疏矩阵!书上有一个方法说:

有一个更微妙的数据结构可以让我们结合 通过使用默认值压缩列表来提高数组访问速度。我们 可以将此结构视为四个数组,如图所示。 3.66.5 基本数组用于确定状态 s 的条目的基本位置,这些条目位于 next 和 check 数组中。 默认数组用于确定替代基准位置,如果 检查数组告诉我们 base[s] 给出的那个是无效的。到 计算 nextState(s, a),输入 a 上状态 s 的转换,我们 检查下一个并检查位置 l = base[s]+ a 中的条目,其中 字符 a 被视为整数,大概在 0 到 127. 如果 check[l] = s,那么这个条目是有效的,并且输入 a 上状态 s 的下一个状态是 next[l]。如果 check[l] != s,那么我们确定 另一个状态 t = default[s] 并重复该过程,就好像 t 是 当前状态。更正式地,函数 nextstate 定义为 如下:

int nextState(s, a) {
if ( check[base[s] + a] = s ) return next[base[s] + a];
else return nextState(default[s], a);
}

我不明白这四个数组是由什么组成的?谁能为我解释一下?你有另一种简单的算法来优化我的稀疏压缩代码吗?我知道 CSR 压缩,但我不知道应该如何使用它们在我的 C# 代码中编写 nextState(s,a) !你知道吗?

【问题讨论】:

    标签: c# compiler-optimization sparse-matrix transitions dfa


    【解决方案1】:

    假设有六个输入符号:字母 a b c d e 和 EOF。假设我们希望我们的语言包含七个字符串:ace add bad bed bee cab dad(每个字符串都必须以 EOF 结尾)。

    为 DFA 存储转换表的最简单方法是二维数组simpleNext。一根轴是状态编号,另一根轴是输入符号编号。当 FA 处于状态 s 并看到符号 a 时,它会移动到状态 simpleNext[s][a]

    这是一个用于识别示例字符串的 DFA 的simpleNext 转换表:

    st#   a  b  c  d  e EOF
      0   0  0  0  0  0  0
      1   2  3  4  5  0  0
      2   0  0 16 17  0  0
      3  11  0  0  0 12  0
      4   9  0  0  0  0  0
      5   6  0  0  0  0  0
      6   0  0  0  7  0  0
      7   0  0  0  0  0  8
      8   0  0  0  0  0  0
      9   0 10  0  0  0  0
     10   0  0  0  0  0  8
     11   0  0  0 15  0  0
     12   0  0  0 13 14  0
     13   0  0  0  0  0  8
     14   0  0  0  0  0  8
     15   0  0  0  0  0  8
     16   0  0  0  0 19  0
     17   0  0  0 18  0  0
     18   0  0  0  0  0  8
     19   0  0  0  0  0  8
    

    状态 0 是错误状态。如果 FA 到达这里,它会在没有从该状态转换的状态下读取一些符号。状态 8(唯一的其他全零行)是唯一的接受状态。

    这里的问题是simpleNext 的大小为|S| × |?|(状态数乘以符号数)= 120。对于真正的 DFA(如在语言解析器中发现的那样),具有更多状态和符号,它将是更大。这些天对于 RAM 来说不是太大,但可能会浪费很多 L1 缓存行。大多数状态没有针对大多数符号的转换,因此表的大部分设置为 0(错误状态)。

    我们想要一种在更小的空间内存储转换表的方法,同时还能让我们非常快速地访问它。

    让我们探索本书中描述的压缩技术的类似但更简单的版本。

    让我们稍微不同地排列表格的行。从第 0 行和第 1 行开始:

      0   0  0  0  0  0  0
      1   2  3  4  5  0  0
    

    然后将第 2 行移至其非零条目(16 和 17)上方只有零:

      0   0  0  0  0  0  0
      1   2  3  4  5  0  0
      2         0  0 16 17  0  0
    

    现在移动第 3 行,直到其非零条目(11 和 12)上方只有零:

      0   0  0  0  0  0  0
      1   2  3  4  5  0  0
      2         0  0 16 17  0  0
      3                    11  0  0  0 12  0
    

    (请注意,我们必须假设每一行都附加了无数个零。)

    对剩余的行一一重复。最后你有这个:

      0   0  0  0  0  0  0
      1   2  3  4  5  0  0
      2         0  0 16 17  0  0
      3                    11  0  0  0 12  0
      4                        9  0  0  0  0  0
      5                           6  0  0  0  0  0
      6                     0  0  0  7  0  0
      7                     0  0  0  0  0  8
      8   0  0  0  0  0  0
      9                                    0 10  0  0  0  0
     10                           0  0  0  0  0  8
     11                                    0  0  0 15  0  0
     12                                       0  0  0 13 14  0
     13                                       0  0  0  0  0  8
     14                                          0  0  0  0  0  8
     15                                             0  0  0  0  0  8
     16                                                   0  0  0  0 19  0
     17                                                         0  0  0 18  0  0
     18                                                      0  0  0  0  0  8
     19                                                         0  0  0  0  0  8
    

    现在写下我们每行移动的数量:

    st# off
      0   0   0  0  0  0  0  0
      1   0   2  3  4  5  0  0
      2   2         0  0 16 17  0  0
      3   6                    11  0  0  0 12  0
      4   7                        9  0  0  0  0  0
      5   8                           6  0  0  0  0  0
      6   6                     0  0  0  7  0  0
      7   6                     0  0  0  0  0  8
      8   0   0  0  0  0  0  0
      9  11                                    0 10  0  0  0  0
     10   8                           0  0  0  0  0  8
     11  11                                    0  0  0 15  0  0
     12  12                                       0  0  0 13 14  0
     13  12                                       0  0  0  0  0  8
     14  13                                          0  0  0  0  0  8
     15  14                                             0  0  0  0  0  8
     16  15                                                   0  0  0  0 19  0
     17  16                                                         0  0  0 18  0  0
     18  17                                                      0  0  0  0  0  8
     19  18                                                         0  0  0  0  0  8
    

    现在将行折叠成一行,用非零覆盖零。将偏移量保留为单独的数组:

        index  0  1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
    nextState  2  3  4  5 16 17 11  9  6  7 12  8 10  8 15 13 14  8  8  8 19 18  8  8
    
         state#  0  1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19
      stateBase  0  0  2  6  7  8  6  6  0 11  8 11 12 12 13 14 15 16 17 18
    

    此时,我们可以使用这两个数组从原始数组中找到任何非零值。对于状态s 和符号编号i,如果simpleNext[s][i] 不为零,则nextState[stateBase[s] + i] 中的值相同。

    这个方案剩下的唯一问题是我们不知道原始表中的哪些值是零。我们可以通过添加另一个数组来解决这个问题,checkState,平行于nextState。对于nextState 的每个元素,checkState 的对应元素告诉我们nextState 最初是从哪个状态复制而来的:

         index  0  1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
     nextState  2  3  4  5 16 17 11  9  6  7 12  8 10  8 15 13 14  8  8  8 19 18  8  8
    checkState  1  1  1  1  2  2  3  4  5  6  3  7  9 10 11 12 12 13 14 15 16 17 18 19
    

    现在,对于某些状态s 和符号编号i,我们可以查看nextState[stateBase[s] + i] 是否属于状态s,或者原始表中状态s 和符号@987654350 是否为零@:

    if (checkState[stateBase[s] + i] == s) {
        return nextState[stateBase[s] + i];
    } else {
        return 0;
    }
    

    原始simpleNext 表包含 6 * 20 = 120 个值。压缩表(stateBasenextStatecheckState 的组合)包含 2 * 23 + 20 = 66 个数字。对于典型的 DFA(如在语言解析器中发现的那样),具有更多状态和符号,压缩比要好得多。

    在实际实现中,我可能会将 nextStatecheckState 存储为单个结构数组,例如

    typedef struct {
        int nextState;
        int checkState;
    } TableEntry;
    
    TableEntry table[NUM_ENTRIES];
    

    无论如何,书中描述的压缩方案是一个更复杂的变体。对我来说已经很晚了,所以我打算离开这里,但我会尝试明天再回来解释这本书的方案与我上面解释的内容有何不同。

    【讨论】:

    • 那我们为什么需要一个“默认”数组呢?它有什么帮助以及我们如何构建它?
    • 查看simpleNext 表(我的答案中的第一个表)。看看有多少行彼此相似?例如。第 5 行与第 0 行几乎完全相同(它们仅在第 0 列不同)。 defaults 数组利用了这种相似性。当您在压缩过程中到达第 5 行时,您只存储第 5 行中与对应的第 0 行条目不同的条目。然后将第 5 行的 defaults 条目设置为 0。解析时,匹配器(通过查看 defaults)知道如果找不到第 5 行所需的条目,则使用第 0 行的条目。
    • 更好的是,第 10 行与第 7 行相同。因此压缩器不必存储任何特定于第 10 行的条目。它只需将 7 存储在第 10 行的 defaults 表中,并且当匹配器需要第 10 行的条目时,它总是会使用第 7 行的条目。
    • 这是一个很有帮助的解释,谢谢@robmayoff。我不确定我是否理解第 3.9.8 节最后一段的要点,它说:“虽然我们可能无法选择基值,以便没有未使用的下一个检查条目,但经验表明,简单的策略依次为状态分配基值,并为每个基[s] 值分配最小整数,这样状态 s 的特殊条目就不会被先前占用,所占用的空间比可能的最小值多一点。”
    • 这是否意味着如果状态 1 和 2 相似,因此状态 1 具有条目 5、6、7,而状态 2 具有条目 5、8、7,那么 2 的基值由下式计算抵消 8?。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2019-03-21
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-07-12
    相关资源
    最近更新 更多