【问题标题】:Using CRC32 algorithm to hash string at compile-time在编译时使用 CRC32 算法对字符串进行哈希处理
【发布时间】:2015-04-24 21:23:36
【问题描述】:

基本上我希望我的代码能够做到这一点:

 Engine.getById(WSID('some-id'));

哪个应该被改造

 Engine.getById('1a61bc96');

就在被编译成 asm 之前。所以在编译时

这是我的尝试

constexpr int WSID(const char* str) {
    boost::crc_32_type result;
    result.process_bytes(str,sizeof(str));
    return result.checksum();
}

但我在尝试使用 MSVC 18(CTP 2013 年 11 月)编译时得到了这个

error C3249: illegal statement or sub-expression for 'constexpr' function

我怎样才能得到WSID函数,使用这种方式或任何方式,只要它是在编译期间完成的?

试过这个:Compile time string hashing

 warning C4592: 'crc32': 'constexpr' call evaluation failed; function will be called at run-time

编辑:

我在 Jason Gregory 的 Game Engine Architecture 中第一次听说这种技术。我联系了作者,他很乐意回答我这个问题:

我们所做的是将我们的源代码通过一个自定义的小型预处理器,该预处理器搜索 SID('xxxxxx') 形式的文本,并将单引号之间的任何内容转换为十六进制文本 (0xNNNNNNNN) . [...]

您也可以通过宏和/或一些模板元编程来完成它,尽管正如您所说,让编译器为您完成这种工作很棘手。这并非不可能,但编写自定义工具更容易、更灵活。 [...]

还请注意,我们为 SID('xxxx') 文字选择了单引号。这样做是为了让我们在代码编辑器中获得一些合理的语法高亮显示,但是如果出现问题并且一些未经预处理的代码通过编译器,它会抛出语法错误,因为单引号通常保留用于单字符文字。

还请注意,让您的小型预处理工具将字符串缓存在某种数据库中是至关重要的,这样可以在给定哈希码的情况下查找原始字符串。当您调试代码并检查StringId 变量时,调试器通常会向您显示相当难以理解的哈希码。但是使用 SID 数据库,您可以编写一个插件,将这些哈希码转换回它们的字符串等价物。这样,您将在监视窗口中看到 SID('foo'),而不是 0x75AE3080 [...]。此外,游戏应该能够加载相同的数据库,以便它可以在屏幕上打印字符串而不是十六进制哈希码以进行调试[...]。

虽然预处理有一些主要优点,但这意味着我必须准备某种修改文件的输出系统(那些将存储在其他地方,然后我们需要告诉 MSVC)。所以它可能会使编译任务复杂化。有没有办法用 python 预处理文件而不用头疼?但这不是问题,我仍然对使用编译时函数感兴趣(关于缓存我可以使用 ID 索引)

【问题讨论】:

  • 在这里查看答案:stackoverflow.com/questions/3226211/…
  • @Nim 所以我要改成一行?
  • @Vinz243 如果您使用的是 C++11 而不是 -14,可以。除非您仍然可以说服编译器在编译时运行非 constexpr 函数。
  • 几年前有一个代码高尔夫可以做到这一点。 codegolf.stackexchange.com/questions/3268/…
  • 第一条错误消息“type not allowed for 'constexpr'”是错误的或具有误导性:std::string const& 是文字类型(因为它是引用),因此允许作为函数参数的类型一个 constexpr 函数。 clang++ 和 g++ 也接受它(在 C++11 模式下)。

标签: c++ c++11 visual-studio-2012 constexpr compile-time


【解决方案1】:

这是一个完全在编译时工作的解决方案,但也可以在运行时使用。它是 constexpr、模板和宏的混合体。您可能需要更改某些名称或将它们放在单独的文件中,因为它们很短。

请注意,我重用了来自 this answer for the CRC table generation 的代码,并且我基于来自 this page 的代码来实现。

我没有在 MSVC 上测试它,因为我目前没有在我的 Windows VM 中安装它,但我相信它应该可以工作,或者至少可以进行一些细微的更改。

这里是代码,你可以直接使用crc32函数,或者更贴近你的问题的WSID函数:

#include <cstring>
#include <cstdint>
#include <iostream>

// Generate CRC lookup table
template <unsigned c, int k = 8>
struct f : f<((c & 1) ? 0xedb88320 : 0) ^ (c >> 1), k - 1> {};
template <unsigned c> struct f<c, 0>{enum {value = c};};

#define A(x) B(x) B(x + 128)
#define B(x) C(x) C(x +  64)
#define C(x) D(x) D(x +  32)
#define D(x) E(x) E(x +  16)
#define E(x) F(x) F(x +   8)
#define F(x) G(x) G(x +   4)
#define G(x) H(x) H(x +   2)
#define H(x) I(x) I(x +   1)
#define I(x) f<x>::value ,

constexpr unsigned crc_table[] = { A(0) };

// Constexpr implementation and helpers
constexpr uint32_t crc32_impl(const uint8_t* p, size_t len, uint32_t crc) {
    return len ?
            crc32_impl(p+1,len-1,(crc>>8)^crc_table[(crc&0xFF)^*p])
            : crc;
}

constexpr uint32_t crc32(const uint8_t* data, size_t length) {
    return ~crc32_impl(data, length, ~0);
}

constexpr size_t strlen_c(const char* str) {
    return *str ? 1+strlen_c(str+1) : 0;
}

constexpr int WSID(const char* str) {
    return crc32((uint8_t*)str, strlen_c(str));
}

// Example usage
using namespace std;

int main() {
    cout << "The CRC32 is: " << hex << WSID("some-id") << endl;
}

第一部分负责生成常量表,而crc32_impl 是一个标准的 CRC32 实现,转换为与 C++11 constexpr 一起使用的递归样式。 那么crc32WSID 只是为了方便起见的简单包装器。

【讨论】:

  • 这很漂亮。您能否再解释一下您对模板类所做的工作?看起来您是递归继承直到 k = 0 然后将最终模板参数定义为值?我不太熟悉模板类专业化以及您在这里所做的事情。
  • @jschultz410 我们基本上两次使用相同的技术,分别是 constexpr 和模板。我们将the reference implementation 转换为递归函数。模板实现了核心循环for (k = 0; k &lt; 8; k++){...},但使用了递归,宏通过简单地调用模板256次来替换外部循环for (n = 0; n &lt; 256; n++) {...}
  • 谢谢!但它说 warning C4592: 'crc32_impl': 'constexpr' call evaluation failed; function will be called at run-time 和 crc32 相同:/
  • @Vinz243 A comment on this post 说“编译器团队已确认此警告是虚假的 - constexpr 评估无论如何都会成功。” this bug report 说它是固定的。它是否适用于最新版本的 MSVC,您是否尝试过使用调试器单步执行?
  • 我需要让 vs2012 工作才能获得调试器(许可证问题)
【解决方案2】:

如果有人感兴趣,我使用 C++14 风格的 constexpr 函数编写了一个 CRC-32 表生成器函数和代码生成器函数。在我看来,结果是比我在互联网上看到的许多其他尝试更易于维护的代码,并且它与预处理器相距甚远。

现在,它确实使用了一个名为 cexp::array 的自定义 std::array 'clone',因为 G++ 似乎没有将 constexpr 关键字添加到它们的非常量引用索引访问/写入运算符中。

但是,它是相当轻量级的,希望在不久的将来该关键字将被添加到 std::array 中。但是现在,非常简单的数组实现如下:

namespace cexp
{

    // Small implementation of std::array, needed until constexpr
    // is added to the function 'reference operator[](size_type)'
    template <typename T, std::size_t N>
    struct array {
        T m_data[N];

        using value_type = T;
        using reference = value_type &;
        using const_reference = const value_type &;
        using size_type = std::size_t;

        // This is NOT constexpr in std::array until C++17
        constexpr reference operator[](size_type i) noexcept {
            return m_data[i];
        }

        constexpr const_reference operator[](size_type i) const noexcept {
            return m_data[i];
        }

        constexpr size_type size() const noexcept {
            return N;
        }
    };

}

现在,我们需要生成 CRC-32 表。我基于一些 Hacker's Delight 代码的算法,它可能可以扩展以支持许多其他的 CRC 算法。但是,唉,我只需要标准实现,所以这里是:

// Generates CRC-32 table, algorithm based from this link:
// http://www.hackersdelight.org/hdcodetxt/crc.c.txt
constexpr auto gen_crc32_table() {
    constexpr auto num_bytes = 256;
    constexpr auto num_iterations = 8;
    constexpr auto polynomial = 0xEDB88320;

    auto crc32_table = cexp::array<uint32_t, num_bytes>{};

    for (auto byte = 0u; byte < num_bytes; ++byte) {
        auto crc = byte;

        for (auto i = 0; i < num_iterations; ++i) {
            auto mask = -(crc & 1);
            crc = (crc >> 1) ^ (polynomial & mask);
        }

        crc32_table[byte] = crc;
    }

    return crc32_table;
}

接下来,我们将表存储在一个全局变量中并对其执行基本的静态检查。这种检查很可能会得到改进,并且没有必要将其存储在全局中。

// Stores CRC-32 table and softly validates it.
static constexpr auto crc32_table = gen_crc32_table();
static_assert(
    crc32_table.size() == 256 &&
    crc32_table[1] == 0x77073096 &&
    crc32_table[255] == 0x2D02EF8D,
    "gen_crc32_table generated unexpected result."
);

现在表格已生成,是时候生成 CRC-32 代码了。我再次基于 Hacker's Delight 链接的算法,目前它只支持来自 c 字符串的输入。

// Generates CRC-32 code from null-terminated, c-string,
// algorithm based from this link:
// http://www.hackersdelight.org/hdcodetxt/crc.c.txt 
constexpr auto crc32(const char *in) {
    auto crc = 0xFFFFFFFFu;

    for (auto i = 0u; auto c = in[i]; ++i) {
        crc = crc32_table[(crc ^ c) & 0xFF] ^ (crc >> 8);
    }

    return ~crc;
}

为了完成,我在下面生成了一段CRC-32代码并静态检查它是否有预期的输出,然后将其打印到输出流中。

int main() {
    constexpr auto crc_code = crc32("some-id");
    static_assert(crc_code == 0x1A61BC96, "crc32 generated unexpected result.");

    std::cout << std::hex << crc_code << std::endl;
}

希望这可以帮助其他任何希望实现 CRC-32 的编译时生成,甚至是一般情况。

【讨论】:

    【解决方案3】:

    @tux3 的回答非常巧妙!但是很难维护,因为您基本上是在预处理器命令中编写自己的 CRC32 实现。

    解决问题的另一种方法是先回过头来了解需求的必要性。如果我理解正确,那么问题似乎是性能。在这种情况下,您可以在第二个时间点调用您的函数而不会影响性能:在程序加载时。在这种情况下,您将访问全局变量而不是传递常量。性能方面,初始化后两者应该是相同的(一个 const 从您的代码中获取 32 位,一个全局变量从常规内存位置获取 32 位)。

    你可以这样做:

    static int myWSID = 0;
    
    // don't call this directly
    static int WSID(const char* str) {
      boost::crc_32_type result;
      result.process_bytes(str,sizeof(str));
      return result.checksum();
    }
    
    // Put this early into your program into the
    // initialization code.
    ...
    myWSID = WSID('some-id');
    

    根据您的整个程序,您可能需要一个内联访问器来检索值。

    如果可以接受较小的性能影响,您也可以这样编写函数,基本上使用单例模式。

    // don't call this directly
    int WSID(const char* str) {
      boost::crc_32_type result;
      result.process_bytes(str,sizeof(str));
      return result.checksum();
    }
    
    // call this instead. Note the hard-coded ID string.
    // Create one such function for each ID you need to
    // have available.
    static int myWSID() {
       // Note: not thread safe!
       static int computedId = 0;
       if (computedId == 0)
          computedId = WSID('some-id');
       return computedId;
    }
    

    当然,如果要求编译时评估的原因是不同的(例如,不希望 some-id 出现在编译代码中),这些技术将无济于事。

    另一种选择是使用 Jason Gregory 对自定义预处理器的建议。如果您将所有 IDS 收集到一个单独的文件中,它可以相当干净地完成。该文件不需要具有 C 语法。我会给它一个扩展名,例如.wsid。自定义预处理器会从中生成一个 .H 文件。

    这是它的外观:

    idcollection.wsid(在自定义预处理器之前):

    some_id1
    some_id2
    some_id3
    

    您的预处理器将生成以下 idcollection.h:

    #define WSID_some_id1 0xabcdef12
    #define WSID_some_id2 0xbcdef123
    #define WSID_some_id3 0xcdef1234
    

    在你的代码中,你会调用

    Engine.getById(WSID_some_id1);
    

    关于此的几点说明:

    • 这假定所有原始 ID 都可以转换为有效标识符。如果它们包含特殊字符,您的预处理器可能需要进行额外的处理。
    • 我注意到您的原始问题不匹配。您的函数返回一个 int,但 Engine.getById 似乎需要一个字符串。我建议的代码将始终使用 int(如果您希望始终使用字符串,则可以轻松更改)。

    【讨论】:

    • 这应该是一条评论。您正在粘贴我的代码,它不能直接回答问题(这是一种解决方法)。但是为第二个提案 +1。
    • 我可能应该将其分解为评论和单独的答案。是的,我确实以您的代码为起点。我的版本和你的版本之间的主要区别在于在初始化期间只运行一次。我看待像你这样的请求的方式是,你有一个潜在的问题要解决,我猜测问题可能是什么,并为此提出了一个替代解决方案。如果那对您没有帮助-没问题。于是提出第二个建议。感谢您的支持!
    • 这可能对我有帮助。但是问题团队的问题是和 crc32 在编译时。因此是第一部分的评论。
    猜你喜欢
    • 1970-01-01
    • 2019-09-19
    • 2014-05-13
    • 1970-01-01
    • 2011-04-25
    • 1970-01-01
    • 2017-04-04
    • 2021-09-19
    • 2018-11-08
    相关资源
    最近更新 更多