快速查找某个值是不是存在于 C 数组中?
Posted
技术标签:
【中文标题】快速查找某个值是不是存在于 C 数组中?【英文标题】:Quickly find whether a value is present in a C array?快速查找某个值是否存在于 C 数组中? 【发布时间】:2014-10-28 23:50:25 【问题描述】:我有一个具有时间关键 ISR 的嵌入式应用程序,它需要遍历一个大小为 256 的数组(最好是 1024,但 256 是最小值)并检查一个值是否与数组内容匹配。在这种情况下,bool
将设置为 true。
微控制器是 NXP LPC4357,ARM Cortex M4 内核,编译器是 GCC。我已经组合了优化级别 2(3 更慢)并将函数放在 RAM 中而不是闪存中。我还使用指针算术和for
循环,它进行向下计数而不是向上计数(检查i!=0
是否比检查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;
做到这一点绝对最快的方法是什么?允许使用内联汇编。其他“不太优雅”的技巧也是允许的。
【问题讨论】:
有没有办法以不同的方式将值存储在数组中?如果您可以对它们进行排序,那么二进制搜索肯定会更快。如果要存储和搜索的数据在一定范围内,可以用位图等表示。 @BitBank:你会惊讶于编译器在过去 30 年中的进步。 ARM 特别是对编译器非常友好。而且我知道 GCC 上的 ARM 可以发出加载多个指令(至少从 2009 年开始) 很棒的问题,人们忘记了现实世界中存在性能很重要的案例。太多次这样的问题都是用“just use stl”来回答的 标题“...遍历数组”具有误导性,因为您实际上只是在搜索给定值。遍历数组意味着要对每个条目进行一些操作。如果成本可以分摊到多次搜索中,排序确实是一种独立于语言实现问题的有效方法。 您确定不能简单地使用二进制搜索或哈希表吗?对 256 个项目进行二分搜索 == 8 次比较。哈希表 == 平均 1 次跳跃(如果您有完美的哈希,则为 1 次跳跃 max)。只有在 1) 有一个不错的搜索算法(O(1)
或 O(logN)
,与 O(N)
相比),并且 2) 你已经将其描述为瓶颈之后,你才应该诉诸汇编优化。
【参考方案1】:
在性能至关重要的情况下,与手动调整的汇编语言相比,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
更新: cmets中有很多怀疑论者认为我的经历是轶事/毫无价值,需要证明。我使用 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)上运行了我的代码和微软编译的 ARM 代码。我运行了 1000000 次循环迭代,但未能找到匹配项,因此所有内容都在缓存中并且很容易测量。
My Code MS Code
Surface RT 297ns 562ns
Surface RT 2 172ns 296ns
在这两种情况下,我的代码运行速度几乎是原来的两倍。大多数现代 ARM CPU 可能会给出类似的结果。
【讨论】:
@LưuVĩnhPhúc - 这通常是正确的,但严格的 ISR 是最大的例外之一,因为您通常比编译器了解更多。 魔鬼的拥护者:有没有量化证据表明这段代码更快? @BitBank:这还不够好。你必须用证据来支持你的主张。 几年前我就吸取了教训。我为 Pentium 上的图形例程制作了一个惊人的优化内部循环,最佳地使用了 U 和 V 管道。将其降低到每个循环 6 个时钟周期(计算和测量),我为自己感到非常自豪。当我针对用 C 编写的相同内容对其进行测试时,C 更快。我再也没有写过另一行英特尔汇编程序。 “那些认为我的经历是轶事/毫无价值并需要证据的 cmets 怀疑论者。” 不要对他们的 cmets 过于消极。展示证据只会让你的好答案变得更好。【参考方案2】:有一个优化它的技巧(我在一次工作面试中被问到这个):
如果数组中的最后一个条目包含您要查找的值,则返回 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;
如果编译器还没有应用它,那么这个函数肯定会这样做。另一方面,它可能会使优化器更难展开循环,因此您必须在生成的汇编代码中验证这一点...
【讨论】:
@ratchetfreak:OP 没有提供有关如何、在何处以及何时分配和初始化这个数组的任何细节,所以我给出了一个不依赖于此的答案。 数组在 RAM 中,但不允许写入。 很好,但是数组不再是const
,这使得它不是线程安全的。似乎要付出高昂的代价。
@EOF:问题中提到过const
?
@barakmanos:如果我将一个数组和一个值传递给您,并询问您该值是否在数组中,我通常不会假设您会修改数组。最初的问题既没有提到const
也没有提到线程,但我认为提到这个警告是公平的。【参考方案3】:
保持表格排序,并使用 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
位。 大约百分之一。
该测试无法承受其重量!
【讨论】:
我真的很喜欢这个解决方案。如果值的位置是敏感信息,则可以将其修改为以固定数量的周期运行,以避免基于时间的取证。 @OregonTrail:基于时间的取证?有趣的问题,但令人遗憾的评论。 您会在加密库中看到这样的展开循环,以防止定时攻击en.wikipedia.org/wiki/Timing_attack。这是一个很好的例子github.com/jedisct1/libsodium/blob/… 在这种情况下,我们防止攻击者猜测字符串的长度。通常攻击者会获取数百万个函数调用样本来执行定时攻击。 @OregonTrail:我支持您基于时间的评论。我不止一次不得不编写以固定周期数执行的加密代码,以避免将信息泄露给基于时间的攻击。 【参考方案4】:您正在寻求优化算法的帮助,这可能会将您推向汇编程序。但是你的算法(线性搜索)不是那么聪明,所以你应该考虑改变你的算法。例如:
perfect hash function binary search完美的哈希函数
如果您的 256 个“有效”值是静态的并且在编译时已知,那么您可以使用 perfect hash function。您需要找到一个哈希函数,将您的输入值映射到 0..n 范围内的一个值,其中您关心的所有有效值都没有冲突 .也就是说,没有两个“有效”值散列到相同的输出值。在寻找一个好的散列函数时,您的目标是:
保持哈希函数相当快。 尽量减少n。可以得到的最小值是 256(最小的完美哈希函数),但这可能很难实现,具体取决于数据。注意高效散列函数,n 通常是 2 的幂,相当于低位的按位掩码(AND 运算)。哈希函数示例:
输入字节的 CRC,以 n 为模。((x << i) ^ (x >> j) ^ (x << k) ^ ...) % n
(根据需要选择尽可能多的i
、j
、k
、...,左移或右移)
然后您制作一个包含 n 个条目的固定表,其中哈希将输入值映射到表中的索引 i。对于有效值,表条目 i 包含有效值。对于所有其他表条目,请确保索引 i 的每个条目都包含其他一些不会散列到 i 的无效值。
然后在你的中断例程中,输入 x:
-
散列 x 到索引 i(在 0..n 范围内)
在表中查找条目 i 并查看它是否包含值 x。
这将比线性搜索 256 或 1024 个值快得多。
我已经written some Python code 寻找合理的哈希函数。
二分查找
如果您对包含 256 个“有效”值的数组进行排序,则可以执行 binary search,而不是线性搜索。这意味着您应该能够只用 8 个步骤(log2(256)
)搜索 256 个条目的表,或者用 10 个步骤搜索一个 1024 个条目的表。同样,这将比线性搜索 256 或 1024 个值快得多。
【讨论】:
谢谢。二分搜索选项是我选择的选项。另请参阅第一篇文章中的早期评论。这在不使用汇编的情况下非常有效。 确实,在尝试优化代码(例如使用汇编或其他技巧)之前,您可能应该看看是否可以降低算法复杂性。通常降低算法复杂度会比尝试缩短几个周期但保持相同的算法复杂度更有效。 一个流行的概念是找到一个有效的哈希例程需要花费太多精力,因此“最佳实践”是二进制搜索。但有时,“最佳实践”还不够好。假设您在数据包的标头到达(但不是其有效负载)的那一刻即时路由网络流量:使用二进制搜索将使您的产品变得非常缓慢。嵌入式产品通常有这样的限制和要求,例如在 x86 执行环境中的“最佳实践”就是嵌入式产品中的“轻松出路”。【参考方案5】:如果事先知道表中的一组常量,则可以使用perfect hashing 来确保只对表进行一次访问。完美哈希确定一个哈希函数 将每个有趣的键映射到一个唯一的槽(该表并不总是密集的,但您可以决定您可以承受的表的不密集程度,不密集的表通常会导致更简单的散列函数)。
通常,特定键集的完美哈希函数相对容易计算;您不希望它冗长而复杂,因为这会争夺时间,也许最好花在进行多次探测上。
完美散列是一种“1-probe max”方案。人们可以概括这一想法,认为应该用计算哈希码的简单性与进行 k 次探测所需的时间进行权衡。毕竟,目标是“最少的总查找时间”,而不是最少的探测或最简单的哈希函数。但是,我从未见过有人构建 k-probes-max 散列算法。我怀疑有人可以做到,但这可能是研究。
另一种想法:如果您的处理器非常快,那么从完美哈希中对内存的一次探测可能会主导执行时间。如果处理器不是很快,那么 k>1 个探针可能是实用的。
【讨论】:
Cortex-M 远不及极快。 事实上在这种情况下他根本不需要任何哈希表。他只想知道某个键是否在集合中,他不想将它映射到一个值。因此,如果完美的哈希函数将每个 32 位值映射到 0 或 1 就足够了,其中“1”可以定义为“在集合中”。 好点,如果他能得到一个完美的哈希生成器来产生这样的映射。但是,那将是“一个极其密集的集合”;我相信他可以找到一个完美的哈希生成器来做到这一点。如果在集合中,他可能最好尝试获得一个完美的散列,该散列产生一些常数 K,如果不在集合中,则产生除 K 之外的任何值。我怀疑即使是后者也很难获得完美的哈希值。 @DavidOngarotable[PerfectHash(value)] == value
如果值在集合中,则为 1,如果不在集合中,则为 0,并且有众所周知的方法可以生成 PerfectHash 函数(例如,请参阅 burtleburtle.net/bob/hash/perfect.html) .试图找到一个哈希函数,将集合中的所有值直接映射为 1,并将集合中所有不存在的值映射为 0,这是一项鲁莽的任务。
@DavidOngaro:一个完美的散列函数有很多“误报”,也就是说,集合中的值 not 将与集合中的值具有相同的散列。因此,您必须有一个由哈希值索引的表,其中包含“in-the-set”输入值。因此,要验证任何给定的输入值,您 (a) 对其进行哈希处理; (b) 使用哈希值进行查表; (c) 检查表中的条目是否与输入值匹配。【参考方案6】:
使用哈希集。它会给 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);
在这个示例实现中,查找时间通常会非常短,但在最坏的情况下可能会达到存储的条目数。对于实时应用程序,您还可以考虑使用二叉树的实现,这将具有更可预测的查找时间。
【讨论】:
这取决于此查找必须执行多少次才能有效。 呃,查找可能会超出数组的末尾。而且这种线性散列具有很高的冲突率——你不可能得到 O(1)。好的哈希集不是这样实现的。 @JimBalter 是的,不是完美的代码。更像是总体思路;可能只是指向现有的哈希集代码。但考虑到这是一个中断服务例程,证明查找不是非常复杂的代码可能很有用。 你应该把它修好,让它环绕我。 完美哈希函数的关键在于它只进行一次探测。期间。【参考方案7】:在这种情况下,可能值得调查Bloom filters。他们能够快速确定某个值不存在,这是一件好事,因为 2^32 个可能的值中的大多数都不在该 1024 元素数组中。但是,有些误报需要额外检查。
由于您的表显然是静态的,因此您可以确定您的 Bloom 过滤器存在哪些误报,并将它们放入完美的哈希中。
【讨论】:
【参考方案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,ARM 有一个索引地址模式,所以这是没有意义的。至于制作指针const
,GCC 已经发现它不会改变。 const
也没有添加任何内容。
@MSalters 好的,我没有用生成的代码进行验证,关键是要表达一些使它在 C 级别更简单的东西,我认为只是管理指针而不是指针和索引 更简单。我只是不同意“const
没有添加任何东西”:它非常清楚地告诉读者该值不会改变。这是很棒的信息。
这是深度嵌入的代码;迄今为止的优化包括将代码从闪存移动到 RAM。然而,它仍然需要更快。在这一点上,可读性不是目标。
@MSalters “ARM 有一个索引地址模式,所以这是没有意义的”——好吧,如果你完全没抓住重点...... OP 写道“我也使用指针算术和 for 循环”。 unwind 并没有用指针替换索引,他只是消除了索引变量,因此在每次循环迭代时都会额外减去。但是 OP 是明智的(与许多回答和评论的人不同)并最终进行了二进制搜索。【参考方案9】:
其他人建议重新组织您的表,在末尾添加一个标记值,或对其进行排序以提供二分搜索。
您说“我还使用指针算术和 for 循环,它进行向下计数而不是向上计数(检查 i != 0
是否比检查 i < 256
更快)。”
我的第一个建议是:摆脱指针运算和递减计数。类似的东西
for (i=0; i<256; i++)
if (compareVal == the_array[i])
[...]
对于编译器来说往往是惯用的。循环是惯用的,循环变量上的数组索引是惯用的。处理指针算术和指针将倾向于混淆编译器的习语,并使其生成与您所写的内容相关的代码,而不是编译器编写者认为最好的代码一般任务的课程。
例如,上面的代码可能被编译成一个循环,从-256
或-255
运行到零,索引&the_array[256]
。可能是在有效的 C 语言中甚至无法表达但与您为其生成的机器的体系结构相匹配的东西。
所以不要微优化。您只是将扳手扔进优化器的工作中。如果您想变得聪明,请研究数据结构和算法,但不要对其表达进行微优化。它会回来咬你,如果不是在当前的编译器/架构上,那么在下一个。
特别是使用指针算法而不是数组和索引对于编译器完全了解对齐、存储位置、别名注意事项和其他内容以及以最适合机器架构的方式进行强度降低等优化是有害的。
【讨论】:
指针上的循环在 C 中是惯用的,良好的优化编译器可以像处理索引一样处理它们。但这整件事没有实际意义,因为 OP 最终进行了二进制搜索。【参考方案10】:这里可以使用向量化,因为它经常在 memchr 的实现中使用。您使用以下算法:
创建重复查询的掩码,长度等于操作系统的位数(64 位、32 位等)。在 64 位系统上,您将重复 32 位查询两次。
一次将列表处理为多条数据的列表,只需将列表转换为更大数据类型的列表并提取值即可。对于每个块,将其与掩码异或,然后与 0b0111...1 异或,然后加 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
【讨论】:
【参考方案11】:如果您可以通过应用程序可用的内存量来容纳值的域,那么最快的解决方案是将您的数组表示为位数组:
bool theArray[MAX_VALUE]; // of which 1024 values are true, the rest false
uint32_t compareVal = 0x1234ABCD;
bool validFlag = theArray[compareVal];
编辑
我对评论家的数量感到震惊。这个帖子的标题是“如何快速找到一个值是否存在于 C 数组中?” 我会坚持我的答案,因为它的答案正是如此。我可以争辩说,这具有最高效的哈希函数(因为地址 === 值)。我已经阅读了 cmets,并且知道明显的警告。毫无疑问,这些警告限制了它可以用来解决的问题的范围,但是对于它确实解决的问题,它的解决效率非常高。
与其直接拒绝这个答案,不如将其视为您可以通过使用哈希函数在速度和性能之间取得更好平衡的最佳起点。
【讨论】:
这是如何获得 4 票的?问题表明它是Cortex M4。这个东西有 136 KB RAM,而不是 262.144 KB。 令人震惊的是,有多少人对明显错误的答案给予了支持,因为回答者只见树木不见森林。对于 OP 的最大情况 O(log n) 当有更好的解决方案可用时,我对烧毁大量内存的程序员感到非常暴躁。每隔 5 年,我的 PC 似乎就会出现内存不足的情况,而 5 年前这个数量已经很多了。 @CraigMcQueen 孩子们这些天。浪费内存。离谱!回到我的时代,我们有 1 MiB 的内存和 16 位的字长。 /s 严厉的批评者是怎么回事? OP 明确指出速度对于这部分代码绝对至关重要,StephenQuan 已经提到了“荒谬的内存量”。【参考方案12】:如果我的答案已经被回答,我很抱歉 - 只是我是一个懒惰的读者。请随意投反对票))
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。据我所知,即使是现代的英特尔 X86 CPU 在内部也使用 RISC 内核(从 X86 即时编译)。 RISC 的主要优化是流水线优化(对于 Intel 和其他 CPU 也是如此),最大限度地减少代码跳转。一种此类优化(可能是主要优化)是“循环回滚”一种。这是非常愚蠢和高效的,即使是英特尔编译器也可以做到这一点 AFAIK。它看起来像:
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),所以它尽可能快(当然不包括哈希表、排序数组和等等,在其他答案中提到,这可能会根据数组大小给出更好的结果。顺便说一下,Cycles Rollback 方法也可以在那里应用。我在这里写的是我认为我在其他人中没有看到的)
此优化的第二部分是该数组项由直接地址获取(在编译阶段计算,请确保使用静态数组),并且不需要额外的 ADD op 来从数组的基地址计算指针。这种优化可能没有显着效果,因为 AFAIK ARM 架构具有加速阵列寻址的特殊功能。但无论如何,最好知道你直接在 C 代码中做了所有最好的事情,对吧?
由于 ROM 的浪费,循环回滚可能看起来很尴尬(是的,如果您的主板支持此功能,您将它放置到 RAM 的快速部分是正确的),但实际上这是基于 RISC 概念的速度回报。这只是计算优化的一般要点 - 您会为了速度而牺牲空间,反之亦然,具体取决于您的要求。
如果您认为 1024 个元素的数组的回滚对您的情况来说牺牲太大,您可以考虑“部分回滚”,例如将数组分成 2 部分,每部分 512 项,或 4x256,等等。
3) 现代 CPU 通常支持 SIMD 操作,例如 ARM NEON 指令集 - 它允许并行执行相同的操作。坦率地说,我不记得它是否适合比较操作,但我觉得可能是,你应该检查一下。谷歌搜索显示可能还有一些技巧,以获得最大速度,请参阅https://***.com/a/5734019/1028256
希望能给你一些新的想法。
【讨论】:
OP 绕过了所有专注于优化线性循环的愚蠢答案,而是对数组进行预排序并进行二进制搜索。 @Jim,显然应该首先进行这种优化。在某些用例中,例如当您没有时间对数组进行排序时,“愚蠢”的答案可能看起来并不那么愚蠢。或者如果你得到的速度还不够 “很明显,这种优化应该首先进行”——显然不是那些努力开发线性解决方案的人。 “你没有时间对数组进行排序”——我不知道这意味着什么。 “或者如果你得到的速度,无论如何都不够”——呃,如果二分搜索的速度“不够”,那么优化线性搜索不会改善它。现在我完成了这个主题。 @JimBalter,如果我遇到像 OP 这样的问题,我当然会考虑使用二进制搜索之类的算法。我只是不能认为OP没有考虑过。 “您没有时间对数组进行排序”意味着对数组进行排序需要时间。如果您需要对每个输入数据集执行此操作,则可能需要比线性循环更长的时间。 “或者,如果您获得的速度仍然不够”意味着以下 - 上面的优化提示可用于加速二进制搜索代码或其他任何东西【参考方案13】:这更像是一个附录而不是一个答案。
我过去遇到过类似案例,但我的数组在相当多的搜索中保持不变。
其中一半,搜索的值不存在于数组中。然后我意识到我可以在进行任何搜索之前应用“过滤器”。
这个“过滤器”只是一个简单的整数,计算ONCE并用于每次搜索。
它是用 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 ...
您可以使用“更好”的哈希算法,但这可能非常快,特别是对于大数字。 也许这可以为您节省更多周期。
【讨论】:
【参考方案14】:确保指令(“伪代码”)和数据(“theArray”)位于单独的 (RAM) 内存中,以便充分利用 CM4 哈佛架构。来自用户手册:
为了优化 CPU 性能,ARM Cortex-M4 具有用于指令(代码)(I)访问、数据(D)访问和系统(S)访问的三个总线。当指令和数据保存在单独的存储器中时,代码和数据访问可以在一个周期内并行完成。当代码和数据保存在同一个内存中时,加载或存储数据的指令可能需要两个周期。
按照这个指南,我观察到速度提高了约 30%(在我的例子中是 FFT 计算)。
【讨论】:
有趣的是,Cortex-M7 有可选的指令/数据缓存,但在此之前绝对没有。 en.wikipedia.org/wiki/ARM_Cortex-M#Silicon_customization.【参考方案15】:我非常喜欢散列。问题当然是要找到一种既快速又使用最少内存(尤其是在嵌入式处理器上)的高效算法。
如果您事先知道可能出现的值,您可以创建一个程序,通过多种算法运行以找到最佳算法 - 或者更确切地说,为您的数据找到最佳参数。
我创建了这样一个程序,您可以在this post 中阅读并获得一些非常快的结果。 16000 个条目大致转换为 2^14 或 14 次比较的平均值,以使用二进制搜索查找值。我明确地针对非常快速的查找 - 平均在
我的平均查找需要大约 60 个周期(在配备英特尔 i5 的笔记本电脑上)使用通用算法(使用一个除以变量)和 40-45 个周期使用专门算法(可能使用乘法)。这应该转化为 MCU 上的亚微秒查找时间,当然取决于它执行的时钟频率。
如果条目数组跟踪条目被访问的次数,它可以在现实生活中进一步调整。如果在计算索引之前对条目数组从访问次数最多到最少进行排序,那么它将通过一次比较找到最常出现的值。
【讨论】:
以上是关于快速查找某个值是不是存在于 C 数组中?的主要内容,如果未能解决你的问题,请参考以下文章