【问题标题】:C++ fastest data structure for multiple searches用于多个搜索的 C++ 最快数据结构
【发布时间】:2017-03-31 05:38:35
【问题描述】:

用 C++ 编码。我需要一个用于一堆排序字符串的数据结构。我将一次性将所有字符串插入其中而不更新它,但我会经常搜索字符串。我只需要查看结构中是否存在给定的字符串。我希望该列表大约有 100 个字符串。 什么是更快的结构?起初我在考虑 hashmap,但我在某处看到对于如此少量的元素,对向量进行二进制搜索会更好(因为它们已排序)。

【问题讨论】:

  • 两者都尝试,配置文件,然后选择最快的解决方案。
  • 只有 100 个字符串,很可能一切都会很快,而您花在这个问题上的时间就浪费了。编写适合您的应用程序逻辑且易于理解、阅读和维护的代码。如果它真的太慢了,你会发现代码的哪些部分是瓶颈。
  • 分析绝对是这里的路..
  • 这对于Bloom Filter 来说可能是一个有趣的应用程序,特别是如果你能承受一些误报匹配。
  • @MarkRansom Bloom Filter 是一种节省空间的算法。如果数据集只有 100 个字符串,为什么要使用这种算法?

标签: c++ vector data-structures hashmap


【解决方案1】:

除非您每秒进行数亿次搜索,否则您将无法分辨其中的差异。 如果您每秒进行数亿次搜索,请尝试使用基数树。它在内存中非常昂贵,但对于这个小数据集来说应该没关系。

一旦你写了它,就对其进行概要分析。

【讨论】:

    【解决方案2】:

    判断哪种结构在特定情况下最快的最佳(也是唯一)方法是使用不同的数据结构实际对其进行基准测试/测量。然后选择最快的。

    或者换句话说:与那些认为自己太聪明而无法衡量的人相比,衡量你的代码会让你更有优势。 ;)

    对于您在问题中提到的 100 个元素等相当小的列表,您使用什么结构/算法并没有太大的区别,因为获得的时间可能可以忽略不计 - 除非您的程序经常执行搜索。

    【讨论】:

      【解决方案3】:

      使用std::unordered_set<std::string>,它非常适合您的情况。如果您还需要按顺序迭代它们,则可以使用 std::set<std::string>

      如果在分析之后发现您将所有时间都花在查询数据结构上,那么是时候提出另一个问题(使用您将使用的精确代码)。

      【讨论】:

        【解决方案4】:

        假设您谈论的是“全尺寸”CPU1,相对于其他的至少解决方案。每次搜索您可能会遭受多次分支错误预测,并且最终可能会多次检查输入字符串中的每个字符(因为您需要在二进制搜索中的每个节点处重复 strcmp)。

        正如有人已经指出的那样,唯一真正了解的方法是衡量 - 但要做到这一点,您仍然需要能够首先弄清楚候选人是什么!此外,并非总是可以在现实场景中进行测量,因为甚至可能不知道这样的场景(例如,设计一个在许多不同情况下广泛使用的库函数)。

        最后,了解什么可能会很快,让您既可以排除您知道表现不佳的候选人,又可以让您用直觉仔细检查您的测试结果:如果某些事情比您预期的要慢得多,那么值得检查一下原因(编译器是否做了一些愚蠢的事情),如果某些事情更快那么也许是时候更新你的直觉了。

        因此,我将尝试真正尝试快速的方法 - 假设 速度真的很重要,您可以花一些时间验证复杂的解决方案。作为基线,一个简单的实现可能需要 100 ns,而真正优化的可能需要 10 ns。因此,如果您为此花费 10 个小时的工程时间,您将不得不调用此函数 4000 亿次,才能赢回 10 个小时5。当您考虑错误风险、维护复杂性和其他开销时,您将需要确保在尝试优化它之前调用此函数很多 万亿 次。这样的功能很少见,但肯定存在4

        也就是说,您缺少帮助设计快速解决方案所需的大量信息,例如:

        1. 您对搜索功能的输入是std::stringconst char * 还是其他?
        2. 平均和最大字符串长度是多少?
        3. 您的大部分搜索会成功还是不成功?
        4. 你能接受一些误报吗?
        5. 字符串集在编译时是否已知,或者您是否可以接受较长的初始化阶段?

        上面的答案可以帮助你划分设计空间,如下所述。

        布隆过滤器

        如果根据 (4),您可以接受(可控)数量的误报2(3) 您的大部分搜索都不会成功,那么您应该考虑使用Bloom Filter。例如,您可以使用 1024 位(128 字节)过滤器,并使用字符串的 60 位散列通过 6 个 10 位函数对其进行索引。这给出了

        这样做的好处是,在哈希计算之外,它独​​立于字符串的长度,并且不依赖于匹配行为(例如,如果字符串倾向于,依赖于重复字符串比较的搜索会变慢有长的公共前缀)。

        如果你可以接受误报,你就完成了 - 但如果你需要它始终正确但预计搜索大多不成功,你可以将它用作过滤器:如果布隆过滤器返回 false em> (通常情况下)你已经完成了,但如果它返回 true,你需要仔细检查下面讨论的始终正确的结构之一。所以常见的情况很快,但总是返回正确的答案。

        完美哈希

        如果大约 100 个字符串的集合在编译时是已知的,或者您可以做一些一次性繁重的工作来预处理字符串,您可以考虑使用完美哈希。如果你有一个编译时已知的搜索集,你可以将字符串插入gperf,它会输出一个哈希函数和查找表。

        例如,我刚刚将 100 个随机英文单词3 输入gperf,它生成了一个哈希函数,只需查看 两个字符 即可唯一区分每个单词,像这样:

        static unsigned int hash (const char *str, unsigned int len)
        {
          static unsigned char asso_values[] =
            {
              115, 115, 115, 115, 115,  81,  48,   1,  77,  72,
              115,  38,  81, 115, 115,   0,  73,  40,  44, 115,
               32, 115,  41,  14,   3, 115, 115,  30, 115, 115,
              115, 115, 115, 115, 115, 115, 115,  16,  18,   4,
               31,  55,  13,  74,  51,  44,  32,  20,   4,  28,
               45,   4,  19,  64,  34,   0,  21,   9,  40,  70,
               16,   0, 115, 115, 115, 115, 115, 115, 115, 115,
              /* most of the table omitted */
            };
          register int hval = len;
        
          switch (hval)
            {
              default:
                hval += asso_values[(unsigned char)str[3]+1];
              /*FALLTHROUGH*/
              case 3:
              case 2:
              case 1:
                hval += asso_values[(unsigned char)str[0]];
                break;
            }
          return hval;
        }
        

        现在您的哈希函数 快速 并且可能很好预测(如果您没有太多长度为 3 或更短的字符串)。要查找字符串,您只需索引哈希表(也由gperf 生成),然后将您得到的内容与输入字符串进行比较。

        在一些合理的假设下,这将尽可能快 - clang 生成如下代码:

        in_word_set:                            # @in_word_set
                push    rbx
                lea     eax, [rsi - 3]
                xor     ebx, ebx
                cmp     eax, 19
                ja      .LBB0_7
                lea     ecx, [rsi - 1]
                mov     eax, 3
                cmp     ecx, 3
                jb      .LBB0_3
                movzx   eax, byte ptr [rdi + 3]
                movzx   eax, byte ptr [rax + hash.asso_values+1]
                add     eax, esi
        .LBB0_3:
                movzx   ecx, byte ptr [rdi]
                movzx   edx, byte ptr [rcx + hash.asso_values]
                cdqe
                add     rax, rdx
                cmp     eax, 114
                ja      .LBB0_6
                mov     rbx, qword ptr [8*rax + in_word_set.wordlist]
                cmp     cl, byte ptr [rbx]
                jne     .LBB0_6
                add     rdi, 1
                lea     rsi, [rbx + 1]
                call    strcmp
                test    eax, eax
                je      .LBB0_7
        .LBB0_6:
                xor     ebx, ebx
        .LBB0_7:
                mov     rax, rbx
                pop     rbx
                ret
        

        这是一大堆代码,但 ILP 数量还算合理。关键路径是通过 3 个相关的内存访问(在 str 中查找 char 值 -> 在哈希函数表中查找 char 的哈希值 -> 在实际哈希表中查找字符串),您预计这通常需要 20 个周期(当然还有 strcmp 时间)。

        尝试

        这个问题的“经典”compsci 解决方案是trie。 trie 可能是解决您的问题的合理方法,尤其是许多不成功的匹配可以在前几个字符内迅速被拒绝(这在很大程度上取决于匹配集的内容和您正在检查的字符串)。

        您需要一个快速的 trie 实现来完成这项工作。总的来说,我觉得这种方法会受到串行依赖的内存访问的限制——每个节点都可能以一种指针追踪的方法被访问,所以你会受到 L1 访问延迟的影响。

        优化strcmp

        几乎所有上述解决方案在某些时候都依赖于strcmp - 例外是允许误报的布隆过滤器。所以你要确保这部分代码是快速的。

        特别是编译器有时可能会内联strcmp 的“内置”版本而不是调用库函数:在快速测试中icc 进行了内联,但clanggcc 选择调用库函数。没有一个简单的规则会更快,但通常库例程通常是 SIMD 优化的,并且对于长字符串可能更快,而内联版本避免函数调用开销并且对于短字符串可能更快。您可以测试这两种方法,并主要强制编译器在您的情况下执行更快的操作。

        更好的是,您可以利用对输入的控制来做得更好 - 例如,如果您可以确保输入字符串将被 null 填充,这样它的长度是 8 的倍数,那么您可以对哈希表(或任何其他结构)中的参考字符串执行相同的操作,并且您可以一次比较字符串 8 个字节。这不仅大大加快了匹配速度,而且大大减少了分支错误预测,因为它本质上量化了循环行为(所有 1-8 个字符的字符串循环一次,等等)。


        1 这里我指的是台式机、服务器、笔记本电脑 CPU,甚至是现代智能手机 CPU,而不是嵌入式设备 MCU 或类似的东西。

        2 允许误报 意味着即使输入字符串不在集合中,您的“集合中”有时也会返回 true。请注意,反过来它永远不会出错:当字符串 is 在集合中时,它 总是 返回 true - 没有 假阴性 .

        3 具体来说,awk 'NR%990==0' /usr/share/dict/american-english > words

        4 例如,在计算的历史上,你的东西strcmp 被调用了多少次?如果再快 1 ns,会节省多少时间?

        5 这在某种程度上将 CPU 时间与工程时间等同起来,这可能相差 1000 倍以上:亚马逊 AWS 每小时收取 0.02 美元的 CPU 时间费用,而且是一位优秀的工程师可以期望每小时 50 美元(在第一世界)。因此(非常粗略!)度量工程时间比 CPU 时间更有价值 2500 倍。因此,也许您需要为 10 小时的工作打上千万次电话才能获得回报……

        【讨论】:

          【解决方案5】:

          这个问题有点模糊,但最快的字符串匹配算法是有限状态机,即 aho-corasick 算法。它是 e Knuth-Morris-Pratt 匹配算法的推广。如果您只是想要一个简单的查找,如果空间很重要,您可以尝试三元树或压缩树(基数树),甚至二分查找。

          【讨论】:

            【解决方案6】:

            这是一个有趣的问题,因为它非常接近 JAVA 字符串池的概念。 Java使用JNI调用C++实现的native对应方法

            字符串池是JVM对string interning概念的具体实现:

            在计算机科学中,字符串实习是一种仅存储每个不同字符串值的副本的方法,该副本必须是不可变的。驻留字符串使某些字符串处理任务更节省时间或空间,但代价是创建或驻留字符串时需要更多时间。不同的值存储在字符串实习池中。

            让我们看看如何在 Java 7 中实现字符串池

            /** 
             * Returns a canonical representation for the string object. 
             * <p> 
             * A pool of strings, initially empty, is maintained privately by the 
             * class <code>String</code>. 
             * <p> 
             * When the intern method is invoked, if the pool already contains a 
             * string equal to this <code>String</code> object as determined by 
             * the {@link #equals(Object)} method, then the string from the pool is 
             * returned. Otherwise, this <code>String</code> object is added to the 
             * pool and a reference to this <code>String</code> object is returned. 
             * <p> 
             * It follows that for any two strings <code>s</code> and <code>t</code>, 
             * <code>s.intern()&nbsp;==&nbsp;t.intern()</code> is <code>true</code> 
             * if and only if <code>s.equals(t)</code> is <code>true</code>. 
             * <p> 
             * All literal strings and string-valued constant expressions are 
             * interned. String literals are defined in section 3.10.5 of the 
             * <cite>The Java&trade; Language Specification</cite>. 
             * 
             * @return  a string that has the same contents as this string, but is 
             *          guaranteed to be from a pool of unique strings. 
             */  
            public native String intern();
            

            当调用 intern 方法时,如果池中已经包含一个与此 String 对象相等的字符串,由 equal 对象确定,则返回池中的字符串。否则,将此对象添加到池中并返回对该字符串对象的引用。

            Java使用JNI调用C++实现的原生StringTable.intern方法

            \openjdk7\jdk\src\share\native\java\lang\String.c

            Java_java_lang_String_intern(JNIEnv *env, jobject this)  
            {  
                return JVM_InternString(env, this);  
            }
            

            \openjdk7\hotspot\src\share\vm\prims\jvm.h

            /* 
            * java.lang.String 
            */  
            JNIEXPORT jstring JNICALL  
            JVM_InternString(JNIEnv *env, jstring str); 
            

            \openjdk7\hotspot\src\share\vm\prims\jvm.cpp

            // String support ///////////////////////////////////////////////////////////////////////////  
            JVM_ENTRY(jstring, JVM_InternString(JNIEnv *env, jstring str))  
              JVMWrapper("JVM_InternString");  
              JvmtiVMObjectAllocEventCollector oam;  
              if (str == NULL) return NULL;  
              oop string = JNIHandles::resolve_non_null(str);  
              oop result = StringTable::intern(string, CHECK_NULL);
              return (jstring) JNIHandles::make_local(env, result);  
            JVM_END
            

            \openjdk7\hotspot\src\share\vm\classfile\symbolTable.cpp

            oop StringTable::intern(Handle string_or_null, jchar* name,  
                                    int len, TRAPS) {  
              unsigned int hashValue = java_lang_String::hash_string(name, len);  
              int index = the_table()->hash_to_index(hashValue);  
              oop string = the_table()->lookup(index, name, len, hashValue);  
              // Found  
              if (string != NULL) return string;  
              // Otherwise, add to symbol to table  
              return the_table()->basic_add(index, string_or_null, name, len,  
                                            hashValue, CHECK_NULL);  
            }
            

            \openjdk7\hotspot\src\share\vm\classfile\symbolTable.cpp

            oop StringTable::lookup(int index, jchar* name,  
                                    int len, unsigned int hash) {  
              for (HashtableEntry<oop>* l = bucket(index); l != NULL; l = l->next()) {  
                if (l->hash() == hash) {  
                  if (java_lang_String::equals(l->literal(), name, len)) {  
                    return l->literal();  
                  }  
                }  
              }  
              return NULL;  
            }
            

            如果您想了解更多有关 Oracle 工程师如何更改 Java 7 中的字符串池逻辑的信息,该链接将对您有用。 Bug Report: make the string table size configurable。字符串池被实现为一个固定容量的映射,每个存储桶包含一个字符串列表,具有相同的代码。默认池大小为 1009。

            对于您的问题,您可以编写一个测试程序,与此方法比较堆数据结构并确定哪个更好。

            【讨论】:

              【解决方案7】:

              Trie 是最适合您的解决方案。 我这么说是因为你没有太多的字符串,所以这样走会更好。 您可以在我的 github 链接中查看我的 trie 实现
              https://github.com/prem-ktiw/Algorithmic-Codes/blob/master/Trie/char_trie.cpp
              该代码有很好的注释,允许您在线性时间内插入一个字符串,并在线性时间内搜索。没有在散列中看到的冲突问题。
              已使用动态分配,因此内存不会成为问题。
              唯一的问题是,在我的实现中,您不能有相同字符串的多个重复副本,并且没有记录 Trie 中有多少字符串副本。
              如果需要任何帮助,我想听听您的意见。

              【讨论】:

                【解决方案8】:

                这取决于你的琴弦有多么不同或它们有什么特定的形状。

                如果您愿意承担内存开销,我认为哈希图是个好主意。对于只有大约 100 个字符串,第一个字符就足够了:

                String* myStrings[256];
                

                您只需查看字符串的第一个字符即可确定它可能在哪个数组中。

                如果您的字符串足够异构(即它们通常不以相同的字母开头),理论上增益是 256 倍速度。损失是内存中额外的 257 个指针(257*64 = 16448 位)。 您可以通过从实际存储的字符串中删除第一个字符来补偿这种损失。

                如果您决定扩大到 2 个或更多字符,则优点和不便都是指数级的。

                String* myStrings[256][256][256];
                

                但是,如果您的字符串很特殊并且不能以任何字符开头或包含任何字符,那么您可以减少数组并将使用的字符映射到插槽。

                char charToSlot[256]; 
                String* myStrings[3];
                

                例如在这种情况下,如果您的字符串只能以字符 100、235 和 201 开头,则 charToSlot[100] = 0、charToSlot[235] = 1 和 charToSlot[201] = 2。

                查找索引稍慢,但对内存的影响很小。如果您操作的字符串只能包含小写字母,那可能会对您有所帮助。那么你对一个角色的理想结构是:

                char charToSlot[256]; 
                String* myStrings[26]; 
                

                而且它可以更容易地放大:

                char charToSlot[256]; 
                String* myStrings[26][26][26]; 
                

                如果您不想对字符串做出任何假设(即它们可以包含任何内容),那么您可以实现一些动态索引(索引在需要时立即添加,并且需要不断重新分配数组) .

                char charToSlot[256]; 
                String**** myStrings; 
                

                另一个技巧,如果您的字符串长度不同并且非常小(5-30 长度),您可以添加一个额外的索引,通过仅搜索具有相同长度的字符串再次增加速度。

                String* myStrings[30][256][256]...
                

                如果您认为这些解决方案过于繁重,那么您可以采用更具统计性的方法。您可以将相同的分支赋予多个字符。例如,“a”、“b”、“c”和“d”都会以相同的方式下降,并且您的分支数量会减少 4 倍。然后你会到达列表并再次检查,一个字符一个字符,如果一个字符串相等,获得你想要的东西的机会就会增加。

                例如,如果您的字符串可以包含所有 256 个字符,但您不想要 256 个而是喜欢 8 个分支,那么您将拥有:

                String* myStrings[8]; 
                

                对于任何角色,您只需将其除以 32(非常快)即可选择分支。这可能是我为您的问题推荐的解决方案,因为您只有大约 100 个字符串,而且您可能不想要一个巨大的数组。

                这一个也可以更好地扩展:

                String* myStrings[8][8][8][8]...
                

                但是存储的数组可能有 32 倍的字符串,并且内容不是确定性的。

                同样,这一切都取决于您的字符串的特定属性,更重要的是您拥有多少个字符串。对于一个非常庞大的字符串数据库,如果它可以将搜索速度提高一个巨大的因素并消除 99.99% 的迭代,那么即使是太比特的映射开销也没有人会关心。

                【讨论】:

                  【解决方案9】:

                  你可以试试binary index array,它是c库索引结构成员字段。

                  教程博客在这里https://minikawoon.quora.com/How-to-search-data-faster-on-big-amount-of-data-in-C-C++

                  示例:-

                  步骤 1. 定义你的结构

                  typedef struct {
                    char book_name[30];
                    char book_description[61];
                    char book_categories[9];
                    int book_code;  
                  } my_book_t;
                  
                  // 160000 size, 10 index field slot
                  bin_array_t *all_books = bin_array_create(160000, 10);
                  

                  步骤 2. 添加索引

                  if (bin_add_index(all_books, my_book_t, book_name, __def_cstr_sorted_cmp_func__)
                  && bin_add_index(all_books, my_book_t, book_categories, __def_cstr_sorted_cmp_func__)
                  && bin_add_index(all_books, my_book_t, book_code, __def_int_sorted_cmp_func__)
                     ) {
                  

                  第 3 步。初始化数据

                      my_book_t *bk = malloc(sizeof(my_book_t));
                      strcpy(bk->book_name, "The Duck Story"));
                      ....
                      ...
                      bin_array_push(all_books, bk );
                  

                  Step 4. 搜索结果 eq, lt(小于), gt(大于)

                  int data_search = 100;
                  bin_array_rs *bk_rs= (my_book_t*) ba_search_eq(all_books, my_book_t,             
                  book_code, &data_search);
                  my_book_t **bks = (my_book_t**)bk_rs->ptrs; // Convert to pointer array
                  // Loop it
                  for (i = 0; i < bk_rs->size; i++) {  
                     address_t *add = bks[i];
                      ....
                  }
                  

                  第 5 步。多重搜索和内部连接或联合

                   // Join Solution
                  bin_array_rs *bk_rs=bin_intersect_rs(
                      bin_intersect_rs(ba_search_gt(...), ba_search_lt(...), true),
                      bin_intersect_rs(ba_search_gt(...), ba_search_lt(....), true),
                                               true);
                  
                   // Union Solution
                  bin_array_rs *bk_rs= bin_union_rs(
                      bin_union_rs(ba_search_gt(...), ba_search_lt(...), true),
                      bin_union_rs(ba_search_gt(...), ba_search_lt(....), true),
                                               true);
                  

                  阅读文档以了解更多关于它如何搜索和搜索后释放内存的详细信息。

                  【讨论】:

                  • 请在答案中添加一个简单的解释,因为提供的链接将来可能会不可用。
                  猜你喜欢
                  • 1970-01-01
                  • 1970-01-01
                  • 1970-01-01
                  • 1970-01-01
                  • 2014-03-09
                  • 2011-10-27
                  • 1970-01-01
                  • 2013-04-21
                  • 2015-10-24
                  相关资源
                  最近更新 更多