快速查找C数组中是否存在值?


124

我有一个具有时间关键ISR的嵌入式应用程序,该应用程序需要循环访问大小为256(最好是1024,但最小为256)的数组,并检查值是否与数组内容匹配。在bool这种情况下,将A 设置为true。

该微控制器是NXP LPC4357,ARM Cortex M4内核,而编译器是GCC。我已经组合了优化级别2(速度慢3),并将函数放在RAM中而不是闪存中。我还使用了指针算术和一个for循环,该循环进行递减计数而不是递增计数(检查if i!=0的速度比检查if的速度快i<256)。总而言之,我最终需要花费12.5 µs的持续时间,因此必须将其大幅缩短以确保可行。这是我现在使用的(伪)代码:

uint32_t i;
uint32_t *array_ptr = &theArray[0];
uint32_t compareVal = 0x1234ABCD;
bool validFlag = false;

for (i=256; i!=0; i--)
{
    if (compareVal == *array_ptr++)
    {
         validFlag = true;
         break;
     }
}

绝对最快的方法是什么?允许使用内联汇编。也可以使用其他“不太优雅”的技巧。


28
有什么方法可以将值不同地存储在数组中吗?如果您可以对它们进行排序,那么二进制搜索肯定会更快。如果要被存储和搜索是在一定范围内的数据,他们可能是可表示用位图,等等
Remo.D

20
@BitBank:您会感到惊讶的是,在过去的三十年中,编译器有多少改进。ARM特别是对编译器友好的。我知道一个事实,即GCC上的ARM可以发出多载指令(至少从2009年开始)
MSalters 2014年

8
令人敬畏的问题,人们忘记了在现实世界中,性能至关重要。太多这样的问题用“ just use stl”回答
Kik

14
标题“ ...遍历数组”具有误导性,因为实际上您只是在搜索给定值。要遍历数组意味着在每个条目上都要做一些事情。如果可以在许多搜索中分摊费用,则排序确实是一种独立于语言实现问题的有效方法。
hardmath 2014年

8
您确定不能简单地使用二进制搜索或哈希表吗?二进制搜索256个项== 8个比较。哈希表==平均1次跳跃(如果您有完美的哈希,则最大跳跃1次)。只有在以下情况下,才应诉诸于程序集优化:1)具有不错的搜索算法(O(1)O(logN)与相比,O(N)),以及2)将其视为瓶颈。
Groo

Answers:


105

在性能至关重要的情况下,与您可以手动调整的汇编语言相比,C编译器很可能不会生成最快的代码。我倾向于走阻力最小的路径-对于像这样的小型例程,我只编写了asm代码,并且很好地知道执行一个循环需要多少个周期。您也许可以摆弄C代码,并使编译器生成良好的输出,但是最终可能会浪费大量时间来调整输出。编译器(尤其是来自Microsoft的编译器)在过去的几年中已经取得了长足的进步,但是它们仍不如您耳边的编译器那么聪明,因为您正在处理的是特定情况,而不仅仅是一般情况。编译器可能未使用某些指令(例如LDM)来加快速度,因此 不可能足够聪明地展开循环。这是一种结合了我在注释中提到的3个想法的方法:循环展开,缓存预取和使用多重加载(ldm)指令。每个数组元素的指令周期数约为3个时钟,但这并未考虑内存延迟。

工作原理: ARM的CPU设计在一个时钟周期内执行大多数指令,但这些指令在流水线中执行。C编译器将尝试通过在其间插入其他指令来消除流水线延迟。当出现像原始C代码这样的紧密循环时,编译器将很难隐藏延迟,因为必须立即比较从内存读取的值。我下面的代码在2组4个寄存器之间交替,以显着减少内存本身和流水线获取数据的延迟。通常,在处理大型数据集时,如果代码没有使用大多数或所有可用寄存器,那么您将无法获得最佳性能。

; r0 = count, r1 = source ptr, r2 = comparison value

   stmfd sp!,{r4-r11}   ; save non-volatile registers
   mov r3,r0,LSR #3     ; loop count = total count / 8
   pld [r1,#128]
   ldmia r1!,{r4-r7}    ; pre load first set
loop_top:
   pld [r1,#128]
   ldmia r1!,{r8-r11}   ; pre load second set
   cmp r4,r2            ; search for match
   cmpne r5,r2          ; use conditional execution to avoid extra branch instructions
   cmpne r6,r2
   cmpne r7,r2
   beq found_it
   ldmia r1!,{r4-r7}    ; use 2 sets of registers to hide load delays
   cmp r8,r2
   cmpne r9,r2
   cmpne r10,r2
   cmpne r11,r2
   beq found_it
   subs r3,r3,#1        ; decrement loop count
   bne loop_top
   mov r0,#0            ; return value = false (not found)
   ldmia sp!,{r4-r11}   ; restore non-volatile registers
   bx lr                ; return
found_it:
   mov r0,#1            ; return true
   ldmia sp!,{r4-r11}
   bx lr

更新: 评论中有很多怀疑者认为我的经历是轶事/毫无价值,需要证明。我使用GCC 4.8(来自Android NDK 9C)通过优化-O2(打开了包括循环展开在内的所有优化)来生成以下输出。我编译了上面问题中显示的原始C代码。这是GCC产生的:

.L9: cmp r3, r0
     beq .L8
.L3: ldr r2, [r3, #4]!
     cmp r2, r1
     bne .L9
     mov r0, #1
.L2: add sp, sp, #1024
     bx  lr
.L8: mov r0, #0
     b .L2

GCC的输出不仅不会展开循环,还浪费了LDR之后停顿的时钟。每个阵列元素至少需要8个时钟。使用地址知道何时退出循环的确很不错,但是在此代码中找不到编译器能够执行的所有神奇操作。我没有在目标平台上运行代码(我没有代码),但是任何有ARM代码性能经验的人都可以看到我的代码更快。

更新2: 我给了Microsoft的Visual Studio 2013 SP2一个机会,以使代码做得更好。它能够使用NEON指令对数组初始化进行矢量化处理,但是由OP编写的线性值搜索与GCC生成的内容相似(我对标签进行了重命名以使其更具可读性):

loop_top:
   ldr  r3,[r1],#4  
   cmp  r3,r2  
   beq  true_exit
   subs r0,r0,#1 
   bne  loop_top
false_exit: xxx
   bx   lr
true_exit: xxx
   bx   lr

正如我说的,我不拥有OP的确切硬件,但是我将在3个不同版本的nVidia Tegra 3和Tegra 4上测试性能,并将结果发布在这里。

更新3: 我在Tegra 3和Tegra 4(Surface RT,Surface RT 2)上运行了我的代码和Microsoft编译的ARM代码。我对一个循环进行了1000000次迭代,但未能找到匹配项,因此所有内容都在缓存中,并且很容易测量。

             My Code       MS Code
Surface RT    297ns         562ns
Surface RT 2  172ns         296ns  

在这两种情况下,我的代码运行速度几乎都快一倍。大多数现代ARM CPU可能会给出相似的结果。


13
@LưuVĩnhPhúc-通常是正确的,但是严格的ISR是最大的例外之一,因为您通常比编译器了解的更多。
sapi 2014年

47
魔鬼的拥护者:是否有任何定量证据证明该代码更快?
奥利弗·查尔斯沃思

11
@BitBank:这还不够。您必须用证据支持您的主张。

13
多年前,我学到了教训。我为Pentium上的图形例程设计了一个令人惊叹的优化内部循环,使用了最佳的U和V管道。将其降低到每个循环6个时钟周期(计算和测量),我为自己感到非常自豪。当我针对用C编写的同一事物进行测试时,C的速度更快。我再也没有写过另一行英特尔汇编程序。
Rocketmagnet 2014年

14
“评论家中的怀疑者认为我的经历是轶事/毫无价值,需要证明。” 不要对他们的评论过于消极。显示证明只会使您的答案更好。
科迪·格雷

87

有一个优化的技巧(一次面试中有人问我):

  • 如果数组中的最后一个条目包含您要查找的值,则返回true
  • 将您要查找的值写到数组的最后一个条目中
  • 迭代数组,直到遇到要查找的值
  • 如果您在数组的最后一个条目之前遇到它,则返回true
  • 返回假

bool check(uint32_t theArray[], uint32_t compareVal)
{
    uint32_t i;
    uint32_t x = theArray[SIZE-1];
    if (x == compareVal)
        return true;
    theArray[SIZE-1] = compareVal;
    for (i = 0; theArray[i] != compareVal; i++);
    theArray[SIZE-1] = x;
    return i != SIZE-1;
}

每次迭代产生一个分支,而不是每次迭代产生两个分支。


更新:

如果允许将数组分配给SIZE+1,则可以摆脱“最后一个条目交换”部分:

bool check(uint32_t theArray[], uint32_t compareVal)
{
    uint32_t i;
    theArray[SIZE] = compareVal;
    for (i = 0; theArray[i] != compareVal; i++);
    return i != SIZE;
}

您还可以theArray[i]改用以下代码摆脱嵌入在中的其他算法:

bool check(uint32_t theArray[], uint32_t compareVal)
{
    uint32_t *arrayPtr;
    theArray[SIZE] = compareVal;
    for (arrayPtr = theArray; *arrayPtr != compareVal; arrayPtr++);
    return arrayPtr != theArray+SIZE;
}

如果编译器尚未应用它,则此函数将确定会应用。另一方面,这可能会使优化程序更难展开循环,因此您必须在生成的汇编代码中进行验证...


2
@ratchetfreak:OP没有提供有关如何以及在何处以及何时分配和初始化该数组的任何详细信息,因此我给出了一个不依赖于此的答案。
barak manos 2014年

3
阵列位于RAM中,但是不允许写入。
wlamers 2014年

1
很好,但是数组不再是const,这使得它不是线程安全的。似乎要付出高昂的代价。
EOF

2
@EOF:问题在哪里const提到过?
barak manos 2014年

4
@barakmanos:如果我向您传递一个数组和一个值,并询问您该值是否在数组中,我通常不认为您会修改该数组。最初的问题既const没有提到线程,也没有提到线程,但是我认为提到这一警告是很公平的。
EOF 2014年

62

您正在寻求优化算法的帮助,这可能会促使您进入汇编器。但是您的算法(线性搜索)不是那么聪明,因此您应该考虑更改算法。例如:

完善的哈希函数

如果您的256个“有效”值是静态的,并且在编译时已知,则可以使用完美的哈希函数。您需要找到一个哈希函数,将您的输入值映射到0 .. n范围内的值,在这里您所关心的所有有效值都没有冲突。也就是说,没有两个“有效”值散列到相同的输出值。在寻找良好的哈希函数时,您的目标是:

  • 保持散列函数相当快。
  • 最小化n。您可以获得的最小值是256(最小的完美哈希函数),但这可能很难实现,具体取决于数据。

请注意,对于高效的哈希函数,n通常是2的幂,等效于低位的按位掩码(AND操作)。哈希函数示例:

  • 输入字节的CRC,取模n
  • ((x << i) ^ (x >> j) ^ (x << k) ^ ...) % n(采摘尽可能多的ijk,...根据需要,用向左或向右移位)

然后,创建一个包含n个条目的固定表,其中哈希将输入值映射到表中的索引i。对于有效值,表条目i包含有效值。对于所有其他表条目,请确保索引i的每个条目都包含其他不会散列到i的无效值

然后在您的中断例程中,输入x

  1. 哈希x索引i(在0..n范围内)
  2. 在表中查找条目i,看看它是否包含值x

这将比线性搜索256或1024个值快得多。

我已经写了一些Python代码来查找合理的哈希函数。

二进制搜索

如果您对256个“有效”值的数组进行排序,则可以执行二进制搜索,而不是线性搜索。这意味着您应该只能在8个步骤(log2(256))中搜索256个条目的表,或者在10个步骤中搜索1024个条目的表。同样,这将比线性搜索256或1024个值快得多。


感谢那。二进制搜索选项是我选择的选项。另请参阅第一篇文章中的早期评论。这无需使用汇编即可很好地完成技巧。
wlamers 2014年

11
确实,在尝试优化代码(例如使用汇编或其他技巧)之前,您可能应该看看是否可以降低算法复杂度。通常,降低算法复杂度比尝试放弃几个周期但保持相同的算法复杂度更为有效。
ysdx 2014年

3
+1用于二进制搜索。重新设计算法是优化的最佳方法。
Rocketmagnet 2014年

一种流行的观点是,找到有效的哈希例程会花费很多精力,因此“最佳实践”是二进制搜索。但是,有时“最佳实践”还不够好。假设您是在数据包的标头到达时(而不是其有效载荷)在运行中动态路由网络流量:使用二进制搜索将使您的产品变慢。嵌入式产品通常具有这样的约束和要求,例如,在x86执行环境中,“最佳实践”就是在嵌入式系统中“走出轻松之路”。
Olof Forshell

60

使表保持排序,并使用Bentley展开的二进制搜索:

i = 0;
if (key >= a[i+512]) i += 512;
if (key >= a[i+256]) i += 256;
if (key >= a[i+128]) i += 128;
if (key >= a[i+ 64]) i +=  64;
if (key >= a[i+ 32]) i +=  32;
if (key >= a[i+ 16]) i +=  16;
if (key >= a[i+  8]) i +=   8;
if (key >= a[i+  4]) i +=   4;
if (key >= a[i+  2]) i +=   2;
if (key >= a[i+  1]) i +=   1;
return (key == a[i]);

重点是,

  • 如果您知道表有多大,那么您知道将有多少次迭代,因此可以完全展开它。
  • 然后,==每次迭代都没有针对该案例的点测试,因为除了最后一次迭代,该案例的概率太低,无法证明需要花费时间进行测试。**
  • 最后,通过将表扩展为2的幂,您最多可以添加一个比较,最多可以添加两个存储。

**如果您不习惯用概率来思考,那么每个决策点都有一个,即您通过执行该所获得的平均信息。对于>=测试,每个分支的概率约为0.5,-log2(0.5)为1,这意味着如果您选择一个分支,您将学习1位,如果您选择另一个分支,则您将学习1位,然后取平均值只是您在每个分支上学到的知识的总和乘以该分支的概率。因此1*0.5 + 1*0.5 = 1>=测试的熵为1。由于您要学习10位,因此需要10个分支。这就是为什么它快!

另一方面,如果您的第一个测试是if (key == a[i+512)怎么办?正确的概率为1/1024,而错误的概率为1023/1024。因此,如果确实如此,您将学习全部10位!但是,如果它是错误的,则您将学习-log2(1023/1024)= .00141位,几乎什么都没有!因此,您可以从该测试中学到的平均10/1024 + .00141*1023/1024 = .0098 + .00141 = .0112位数。大约百分之一。 那个测试没有发挥作用!


4
我真的很喜欢这个解决方案。如果值的位置是敏感信息,可以将其修改为以固定数量的周期运行,以避免基于时间的取证。
OregonTrail 2014年

1
@OregonTrail:基于时间的取证?好玩的问题,但可悲的评论。
Mike Dunlavey 2014年

16
您会在加密库中看到这样的展开循环,以防止Timing Attacks en.wikipedia.org/wiki/Timing_attack。这是一个很好的例子github.com/jedisct1/libsodium/blob/…在这种情况下,我们防止攻击者猜测字符串的长度。通常,攻击者将获取功能调用的数百万个样本来执行定时攻击。
OregonTrail 2014年

3
+1好极了!不错的展开搜索。我以前没看过 我可能会用它。
Rocketmagnet 2014年

1
@OregonTrail:我赞同您基于时间的评论。为了避免将信息泄露给基于定时的攻击,我不止一次不得不编写以固定数量的周期执行的密码代码。
TonyK,2014年

16

如果表中的常数集是事先已知的,则可以使用完美的哈希来确保仅对表进行一次访问。完美的哈希确定将每个有趣的键映射到唯一插槽的哈希函数(该表并不总是密集的,但是您可以决定如何负担得起的表的密度,而密度较小的表通常会导致更简单的哈希函数)。

通常,针对特定键集的完美哈希函数相对易于计算;您不希望它变得冗长而复杂,因为这会争夺时间,也许最好花时间进行多次探查。

完美散列是一种“ 1-probe max”方案。可以用一种思想来概括这一思想,即认为应该将计算哈希码的简单性与进行k次探测所需的时间进行权衡。毕竟,目标是“最少的总​​查找时间”,而不是最少的探测或最简单的哈希函数。但是,我从未见过有人构建k-probes-max哈希算法。我怀疑有人可以做到,但这很可能是研究。

另一个想法是:如果您的处理器非常快,那么从一次完美的哈希中对内存的一次探测可能会占据执行时间。如果处理器不是非常快,则可能不建议使用k> 1个探针。


1
Cortex-M远远不够快
MSalters 2014年

2
实际上,在这种情况下,他根本不需要任何哈希表。他只想知道某个键是否在集合中,他不想将其映射到值。因此,如果完美的哈希函数将每个32位值映射到0或1(其中“ 1”可以定义为“在集合中”)就足够了。
David Ongaro 2014年

1
好点,如果他可以得到一个完美的哈希生成器来产生这样的映射。但是,那将是“极其密集的集合”;我怀疑他可以找到一个完美的哈希生成器来做到这一点。他可能会尝试获得一个完美的散列,如果在集合中生成一个常数K,而在集合中不包含K的任何值,则可能会更好。我怀疑即使对于后者,也很难获得完美的哈希值。
Ira Baxter 2014年

table[PerfectHash(value)] == value如果值在集合中,则@DavidOngaro会产生1,如果值不在集合中,则将产生0,并且有众所周知的产生PerfectHash函数的方法(请参见burtleburtle.net/bob/hash/perfect.html)。试图找到一个将集合中的所有值直接映射为1,而不将集合中的所有值都直接映射为0的哈希函数是一项艰巨的任务。
Jim Balter

@DavidOngaro:完美的哈希函数具有许多“误报”,也就是说,不在集合中的值将具有与集合中的值相同的哈希值。因此,您必须有一个由哈希值索引的表,其中包含“设置中”输入值。因此,要验证任何给定的输入值,您(a)对其进行哈希处理;(b)使用哈希值进行表查找;(c)检查表中的条目是否与输入值匹配。
Craig McQueen

14

使用哈希集。它会给O(1)查找时间。

以下代码假定您可以将值保留0为“空”值,即在实际数据中不存在。对于不是这种情况的情况,可以扩展解决方案。

#define HASH(x) (((x >> 16) ^ x) & 1023)
#define HASH_LEN 1024
uint32_t my_hash[HASH_LEN];

int lookup(uint32_t value)
{
    int i = HASH(value);
    while (my_hash[i] != 0 && my_hash[i] != value) i = (i + 1) % HASH_LEN;
    return i;
}

void store(uint32_t value)
{
    int i = lookup(value);
    if (my_hash[i] == 0)
       my_hash[i] = value;
}

bool contains(uint32_t value)
{
    return (my_hash[lookup(value)] == value);
}

在此示例实现中,查找时间通常将非常短,但是在最坏的情况下,查找时间可能会高达所存储条目的数量。对于实时应用程序,您还可以考虑使用二叉树的实现,它将具有更可预测的查找时间。


3
要使此查询有效,必须取决于执行此查询的次数。
maxywb 2014年

1
嗯,查找可以从数组末尾开始。而且这种线性哈希具有很高的冲突率-您无法获得O(1)。好的哈希集不是这样实现的。
2014年

@JimBalter是的,不是完美的代码。更像是总体思路;本来可以指向现有的哈希集代码。但是考虑到这是一个中断服务程序,可能有必要证明查找不是很复杂的代码。
jpa 2014年

您应该修复它,以使它环绕我。
2014年

完美的散列函数的要点是它可以进行一次探测。期。
Ira Baxter

10

在这种情况下,值得研究Bloom过滤器。它们能够快速确定不存在值,这是一件好事,因为大多数2 ^ 32个可能的值不在该1024个元素数组中。但是,有些误报需要进一步检查。

由于您的表显然是静态的,因此您可以确定Bloom筛选器存在哪些误报,并将其误认为是完美的哈希。


1
有趣的是,我之前从未见过Bloom过滤器。
Rocketmagnet 2014年

8

假设您的处理器以204 MHz的频率运行(这似乎是LPC4357的最大值),并且还假设您的计时结果反映了平均情况(遍历的阵列的一半),我们得到:

  • CPU频率:204 MHz
  • 周期:4.9 ns
  • 周期持续时间:12.5 µs / 4.9 ns = 2551周期
  • 每次迭代的周期:2551/128 = 19.9

因此,您的搜索循环每次迭代花费大约20个周期。这听起来并不可怕,但是我想为了使其更快,您需要查看一下程序集。

我建议删除索引并改为使用指针比较,并制作所有指针const

bool arrayContains(const uint32_t *array, size_t length)
{
  const uint32_t * const end = array + length;
  while(array != end)
  {
    if(*array++ == 0x1234ABCD)
      return true;
  }
  return false;
}

至少值得测试。


1
-1,ARM具有索引地址模式,因此这毫无意义。至于制作指针const,GCC已经发现它没有改变。该constdoesnt't添加任何东西要么。
MSalters 2014年

11
@MSalters OK,我没有生成的代码验证,该点是要表达的东西,使得在C级更简单,我认为只是管理的指针,而不是一个指针和索引简单。我只是不同意“ const不添加任何东西”:它非常清楚地告诉读者,价值不会改变。那是很棒的信息。
放松

9
这是深层嵌入的代码;到目前为止,优化包括将代码从闪存移动到RAM。但是它仍然需要更快。在这一点上,可读性不是目标。
MSalters 2014年

1
@MSalters“ ARM具有索引地址模式,因此这毫无意义”-好吧,如果您完全错过了重点……OP写道“我也使用指针算术和for循环”。unwind并没有用指针代替索引,他只是消除了index变量,因此在每次循环迭代中都增加了一个减法。但是OP是明智的(与许多人回答和评论不同),最终进行了二进制搜索。
Jim Balter

6

其他人建议重新组织表,在末尾添加一个哨兵值,或对其进行排序以提供二进制搜索。

您声明“我还使用了指针算术和for循环,它执行递减计数而不是递增计数(检查是否i != 0比检查是否快i < 256)。”

我的第一个建议是:摆脱指针算法和递减计数。像东西

for (i=0; i<256; i++)
{
    if (compareVal == the_array[i])
    {
       [...]
    }
}

往往是编译器惯用的。循环是惯用的,并且在循环变量上对数组的索引是惯用的。与指针算术和指针打交道将趋于混淆编译器的惯用语,并使编译器生成与编写的内容相关的代码,而不是与编译器编写者认为是一般任务的最佳课程有关的代码。

例如,上面的代码可能被编译成一个循环,从零开始-256-255从零开始运行,索引为off &the_array[256]。可能某些东西甚至无法在有效的C语言中表达,但与您要生成的机器的体系结构相匹配。

因此,请勿进行微优化。您只是将扳手投入到优化器的工作中。如果您想变得聪明一点,请研究数据结构和算法,但不要对它们的表达进行微优化。如果不是在当前的编译器/体系结构上,然后在下一个上,它将再次咬住您。

特别是使用指针算法而不是数组和索引是有毒的,因为编译器完全了解对齐,存储位置,别名注意事项和其他内容,并且以最适合机器体系结构的方式进行了诸如强度降低的优化。


指针循环在C语言中是惯用的,良好的优化编译器可以像处理索引一样处理它们。但是这整个过程都没有意义,因为OP最终进行了二进制搜索。
2014年

3

在这里可以使用向量化,因为在memchr的实现中经常使用向量化。您使用以下算法:

  1. 创建重复的查询掩码,其长度等于操作系统的位数(64位,32位等)。在64位系统上,您将重复32位查询两次。

  2. 只需将列表转换为更大数据类型的列表并提取值,即可一次将列表作为多个数据的列表进行处理。对于每个块,将其与掩码进行XOR,然后与0b0111 ... 1进行XOR,然后加1,再与&重复0b1000 ... 0的掩码。如果结果为0,则肯定没有匹配项。否则,可能(通常很有可能)存在匹配项,因此请正常搜索块。

实施示例:https : //sourceware.org/cgi-bin/cvsweb.cgi/src/newlib/libc/string/memchr.c?rev=1.3&content-type=text/x-cvsweb-markup&cvsroot=src


3

如果您可以使用应用程序可用的内存量来容纳值的域,那么最快的解决方案是将数组表示为位数组:

bool theArray[MAX_VALUE]; // of which 1024 values are true, the rest false
uint32_t compareVal = 0x1234ABCD;
bool validFlag = theArray[compareVal];

编辑

我对评论家的数量感到震惊。该线程的标题为“如何快速查找C数组中是否存在值?”我将坚持我的回答,因为它恰好回答了这一点。我可以争辩说,这具有最高效的哈希函数(因为address === value)。我已经阅读了评论,并且知道明显的警告。无疑,这些警告限制了可以解决的问题范围,但是,对于确实解决的问题,它可以非常有效地解决。

与其完全拒绝这个答案,不如将其视为可以通过使用哈希函数在速度和性能之间实现更好的平衡而发展的最佳起点。


8
如何获得4票赞成票?问题指出这是Cortex M4。这个东西有136 KB RAM,而不是262.144 KB。
MSalters 2014年

1
令人惊讶的是,由于答疑人错过了树林,树木给了答案很多,显然答案是错误的。对于OP的最大情况,O(log n)<< O(n)。
msw 2014年

3
当有更好的解决方案可用时,我会非常讨厌那些消耗大量内存的程序员。每隔5年,我的PC似乎内存不足,而5年前的内存已经足够了。
Craig McQueen 2014年

1
这些天@CraigMcQueen孩子们。浪费内存。太离谱了!在我的时代,我们有1 MiB的内存和16位的字长。/ s
科尔·约翰逊

2
严厉的批评家怎么了?OP明确指出,对于这部分代码,速度绝对至关重要。StephenQuan已经提到了“可笑的内存量”。
Bogdan Alexandru

1

确保指令(“伪代码”)和数据(“ theArray”)位于单独的(RAM)存储器中,以便充分利用CM4 Harvard体系结构的潜力。从用户手册中:

在此处输入图片说明

为了优化CPU性能,ARM Cortex-M4具有三条总线,分别用于指令(代码)(I)访问,数据(D)访问和系统(S)访问。当指令和数据保存在单独的存储器中时,则可以在一个周期内并行进行代码和数据访问。当代码和数据保存在同一内存中时,加载或存储数据的指令可能需要两个周期。


有趣的是,Cortex-M7具有可选的指令/数据缓存,但在此之前绝对没有。 en.wikipedia.org/wiki/ARM_Cortex-M#Silicon_customization
彼得·科德斯

0

抱歉,如果我的答案已经回答-我只是一个懒惰的读者。请随意投票吧))

1)您可以完全删除计数器“ i”-仅比较指针,即

for (ptr = &the_array[0]; ptr < the_array+1024; ptr++)
{
    if (compareVal == *ptr)
    {
       break;
    }
}
... compare ptr and the_array+1024 here - you do not need validFlag at all.

尽管所有这些都不会带来任何明显的改善,但这种优化可能可以通过编译器本身来实现。

2)正如其他答案已经提到的那样,几乎所有现代CPU都是基于RISC的,例如ARM。据我所知,甚至是现代的Intel X86 CPU都在内部使用RISC内核(从X86快速编译)。RISC的主要优化是流水线优化(以及英特尔和其他CPU的优化),可最大程度地减少代码跳转。这种优化的一种类型(可能是主要的优化)是“循环回滚”。这是非常愚蠢且高效的,甚至英特尔编译器也可以做到这一点。看起来像:

if (compareVal == the_array[0]) { validFlag = true; goto end_of_compare; }
if (compareVal == the_array[1]) { validFlag = true; goto end_of_compare; }
...and so on...
end_of_compare:

这样的优化方式是,在最坏的情况下(如果数组中不存在compareVal)流水线不会中断,因此它是尽可能快的(当然不包括算法优化,例如哈希表,排序数组等),在其他答案中提到过,这可能会根据数组大小提供更好的结果。循环回滚方法也可以在此应用。我在这里写的是我认为我在其他人中没有看到的)

此优化的第二部分是,该数组项由直接地址(在编译阶段计算,请确保您使用静态数组)获取,并且不需要其他ADD op即可从数组的基地址计算指针。由于AFAIK ARM体系结构具有加快阵列寻址速度的特殊功能,因此这种优化可能不会产生明显的效果。但是总而言之,最好还是直接在C代码中做到最好,对吧?

由于ROM的浪费,Cycle Rollback可能看起来很尴尬(是的,如果您的主板支持此功能,您确实正确地将其放置在RAM的快速部分中),但是实际上,这是基于RISC概念的合理速度。这只是计算优化的一般要点-您会为了速度而牺牲空间,反之亦然,这取决于您的要求。

如果您认为针对1024个元素的数组的回滚对于您的情况而言是太大的牺牲,则可以考虑“部分回滚”,例如将数组分为2个部分,每个512个项,或4x256,依此类推。

3)现代CPU通常支持SIMD ops,例如ARM NEON指令集-它允许并行执行相同的ops。坦白地说,我不记得它是否适合进行比较操作,但我认为可能应该进行检查。谷歌搜索表明,为了获得最大速度,可能还会有一些技巧,请参阅https://stackoverflow.com/a/5734019/1028256

我希望它能给您一些新的想法。


OP绕过了所有专注于优化线性循环的愚蠢答案,而是对数组进行了预排序并进行了二进制搜索。
Jim Balter

@Jim,很明显,应该首先进行这种优化。在某些用例中,例如当您没有时间对数组进行排序时,“愚蠢”的答案似乎并不愚蠢。或者,如果您获得的速度仍然不够用
Mixaz 2014年

“很明显,应该首先进行这种优化” –显然不适合那些致力于开发线性解决方案的人们。“您没有时间对数组进行排序”-我不知道那是什么意思。“或者,如果您获得的速度仍然不够” –呃,如果二进制搜索的速度“不够”,那么进行优化的线性搜索将不会改善它。现在我已经完成了这个主题。
Jim Balter 2014年

@JimBalter,如果我遇到OP之类的问题,我当然会考虑使用二进制搜索之类的算法。我只是无法想到OP尚未考虑过。“您没有时间对数组进行排序”意味着对数组进行排序需要时间。如果需要对每个输入数据集执行此操作,则它可能需要比线性循环更长的时间。“或者,如果您获得的速度仍然不够”,则表示以下情况-上面的优化提示可用于加快二进制搜索代码的速度或其他速度
Mixaz 2014年

0

我是哈希的忠实粉丝。当然,问题是找到一种既快速又使用最少内存(尤其是在嵌入式处理器上)的高效算法。

如果您事先知道可能出现的值,则可以创建一个程序,该程序通过多种算法来运行,以找到最佳算法-或更确切地说,为数据提供最佳参数。

我创建了一个程序,您可以在本文中阅读它,并获得了非常快速的结果。16000个条目将大致转换为2 ^ 14或14个比较的平均值,以使用二进制搜索找到该值。我明确地打算进行非常快速的查找-平均在<= 1.5查找中找到该值-这导致对RAM的更高要求。我相信,采用较为保守的平均值(例如<= 3)可以节省大量内存。通过比较,对256个或1024个条目进行二进制搜索的平均情况将分别导致平均比较数分别为8和10。

我的平均查询大约需要60个周期(在使用Intel i5的笔记本电脑上),并使用通用算法(利用变量除以一),而使用专业算法(可能使用乘法)则需要40-45个周期。当然,这将取决于您执行MCU的时钟频率,从而使您在MCU上的查找时间达到亚微秒。

如果条目数组跟踪条目被访问了多少次,则可以进行实际调整。如果在计算索引之前,将入口数组从访问量最大到访问量排序,那么它将通过一次比较找到最常出现的值。


0

这更像是附录而不是答案。

过去我也遇到过类似的情况,但是在相当多的搜索中,我的数组保持不变。

其中有一半的搜索值不在数组中。然后我意识到我可以在进行任何搜索之前应用“过滤器”。

该“过滤器”只是一个简单的整数,仅计算一次就可以在每次搜索中使用。

它是用Java编写的,但是非常简单:

binaryfilter = 0;
for (int i = 0; i < array.length; i++)
{
    // just apply "Binary OR Operator" over values.
    binaryfilter = binaryfilter | array[i];
}

因此,在执行二进制搜索之前,请检查binaryfilter:

// Check binaryfilter vs value with a "Binary AND Operator"
if ((binaryfilter & valuetosearch) != valuetosearch)
{
    // valuetosearch is not in the array!
    return false;
}
else
{
    // valuetosearch MAYBE in the array, so let's check it out
    // ... do binary search stuff ...

}

您可以使用“更好”的哈希算法,但这可能非常快,特别是对于大数。也许这可以节省您更多的周期。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.