多个搜索的C ++最快数据结构

时间:2016-11-16 20:48:22

标签: c++ vector data-structures hashmap

用C ++编码。我需要一堆排序字符串的数据结构。我将一次性插入所有字符串而不更新它,但我会经常搜索字符串。我只需要查看结构中是否存在给定字符串。我期待这个列表大约有100个字符串。 什么是更快的结构?我最初想的是hashmap,但是我看到某个地方对于这么少的元素,对向量的二元搜索会更好(因为它们已经排序)。

9 个答案:

答案 0 :(得分:17)

假设你在谈论"全尺寸" CPU 1 ,通过字符串进行二进制搜索,即使只有100个元素也可能相当慢,至少相对于其他解决方案而言。对于每次搜索,您可能会遇到几个分支错误预测,并且最终可能会多次检查输入字符串中的每个字符(因为您需要在二分查找中的每个节点重复strcmp)。

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

最后,了解什么可能是快速的,让你们既可以消除你知道会表现不佳的候选人,也可以让你用自己的直觉仔细检查你的测试结果:如果某些事情比你想象的要慢得多,那么它就会消失。值得检查为什么(编译器做了一些愚蠢的事情),如果某些事情更快那么也许是时候更新你的直觉了。

所以我会尝试实际考虑快速的事情 - 假设速度真的很重要,你可以花一些时间来验证一个复杂的解决方案。作为基线,直接实现可能需要100 ns,而真正优化的实现可能需要10 ns。因此,如果您花费10个小时的工程时间,那么您必须将此功能称为 4000亿次才能获得10个小时的支持 5 。当您考虑到错误风险,维护复杂性和其他开销时,您将需要确保在尝试优化它之前多次调用此函数 trillions 。这些功能很少见,但它们肯定存在 4

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

  1. 您对搜索功能的输入是std::string还是const char *还是别的?
  2. 平均和最大字符串长度是多少?
  3. 您的大部分搜索都会成功或不成功吗?
  4. 你能接受一些误报吗?
  5. 在编译时是否知道字符串集,或者在初始化阶段很长时间你是否正常?
  6. 上面的答案可以帮助您按如下所述划分设计空间。

    布隆过滤器

    如果每(4),您可以接受(可控)数量的误报 2 (3) 您的大多数搜索都不会成功,那么您应该考虑Bloom Filter。例如,您可以使用1024位(128字节)过滤器,并使用字符串的60位散列来索引6个10位函数。这给出了<误报率为1%。

    这具有以下优点:在散列计算之外,它独​​立于字符串的长度,并且它不依赖于匹配行为(例如,依赖于重复字符串比较的搜索将如果字符串倾向于具有长公共前缀,则会变慢。)

    如果你可以接受误报,那么你就完成了 - 但是如果你需要它总是正确的但是期望大多数不成功的搜索,你可以将它用作过滤器:如果bloom过滤器返回 false (通常的情况)你完成了,但是如果它返回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个相关的内存访问(在char中查找str值 - >在哈希函数表中查找char的哈希值 - >查找字符串在实际的哈希表中,你可以预期这通常需要20个周期(当然加上strcmp时间。)

    Trie树

    "经典" complci解决这个问题的方法是trie。对于你的问题,trie可能是一种合理的方法,特别是许多不成功的匹配可以在前几个字符内快速拒绝(这在很大程度上取决于匹配集的内容和你正在检查的字符串)。

    您需要一个快速的trie实现来完成这项工作。总的来说,我觉得这种方法会受到依赖于串行的存储器访问的限制 - 每个节点很可能以一种指针追逐的方式被访问,所以你会遭受很多L1访问延迟。

    优化strcmp

    几乎所有上述解决方案在某些时候都依赖于strcmp - 例外是允许误报的布隆过滤器。因此,您希望确保代码的这一部分速度很快。

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

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

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

    2 允许误报表示如果您的"处于设置状态"即使输入字符串不在集合中,有时也会返回true。请注意,它反而永远不会出错:当 中的字符串 时,始终返回true - 没有 false negatives

    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小时的工作才能获得回报......

答案 1 :(得分:15)

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

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

对于你在问题中提到的100个元素这样的相当小的列表,你使用的结构/算法并没有多大区别,因为获得的时间可能是微不足道的 - 除非你的程序经常执行搜索。

答案 2 :(得分:3)

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

字符串池是JVM对string interning概念的特定实现:

  

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

让我们看看如何在Java 7中实现String池

/** 
 * 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对象的字符串(由等于对象确定),则返回池中的字符串。否则,将此对象添加到池中,并返回对此字符串对象的引用。

Java使用JNI调用由C ++实现的本机StringTable.intern方法

\ openjdk7 \ JDK \ SRC \共享\天然\ java中\郎\ String.c

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

\ openjdk7 \热点\ SRC \共享\ VM \ prims \ jvm.h

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

\ openjdk7 \热点\ SRC \共享\ 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 \热点\ SRC \共享\ VM \类文件\ 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 \热点\ SRC \共享\ VM \类文件\ 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。

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

答案 3 :(得分:2)

除非你每秒进行数亿次搜索,否则你无法区分它们。 如果您每秒进行数亿次搜索,请尝试使用基数树。它的内存非常昂贵,但这个小数据集并不重要。

编写完成后,对其进行分析。

答案 4 :(得分:2)

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

答案 5 :(得分:2)

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

如果您愿意接受内存开销,我认为散列图是一个好主意。对于大约100个字符串,第一个字符就足够了:

String* myStrings[256];

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

如果你的字符串足够异质(即它们通常不是以相同的字母开头),那么增益在理论上是256倍速。丢失a是内存中额外的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]...

如果您认为这些解决方案太重,那么您可以采用更加统计的方法。您可以将相同的分支分配给多个字符。例如&#39; a&#39;,&#39;&#39;&#39; c&#39;并且&#39; d&#39;所有人都会以同样的方式走下去,你的分支会减少4倍。然后你到达列表并再次检查,如果一个字符串是相同的,则char,char,并且增加获得你想要的机会。

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

String* myStrings[8]; 

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

此外,这个扩展得更好:

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

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

同样,所有这些都取决于字符串的特定属性,更重要的是取决于您拥有多少字符串。对于一个非常庞大的字符串数据库,如果用巨大的因子提高搜索速度并删除99.99%的迭代,没有人会关心甚至是太比特的映射开销。

答案 6 :(得分:1)

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

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

答案 7 :(得分:1)

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

答案 8 :(得分:0)

您可以尝试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 );

步骤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);

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

相关问题