memmove 是不是移动元素(与 for 循环相同),还是一次抓取整个内存块?
Posted
技术标签:
【中文标题】memmove 是不是移动元素(与 for 循环相同),还是一次抓取整个内存块?【英文标题】:Does memmove shift elements (the same way a for-loop does), or does it grab an entire block of memory at once?memmove 是否移动元素(与 for 循环相同),还是一次抓取整个内存块? 【发布时间】:2014-02-08 02:45:42 【问题描述】:在我的算法课中,我们必须上交用于删除整数列表重复项的算法,并尽可能降低复杂度。在我的算法中,当我看到一个重复的整数时,我将该整数之后的每个元素向下移动一个索引,以便使用 for 循环删除重复的元素;像这样:
for(int i=dup_index; i<arr_size-1; i++)
arr[i] = arr[i+1];
我的算法使用 memmove 会更有效吗?此外,如果设计算法是我的工作,并且假设 memmove 降低了我的算法的复杂性,那么使用 memmove 是否会被视为“作弊”?
【问题讨论】:
无论哪种方式都将是线性复杂度。memmove
可能更快,但话又说回来,它可能不会。在大多数典型计算机上,内存带宽将是主要瓶颈,因此最简单的代码和最精心优化的代码之间的差异很少超过 15% 到可能 20%。
在 C++ 中使用 'std::move' 而不是 'memmove',但这两种方法都不是解决这个问题的最佳选择。
您应该关注的算法是最小化项目移动的次数完全。想象一下,您所呈现的数据集的所有其他元素(或更糟糕的是,每个元素)都是 same 值。首先移动不应该出现在输出中的元素并将其最小化是本练习的重点。 (以及相当数量的指针行走)。
说 memmove 降低了复杂性是骗人的,即使它在实践中会更快。您经常使用比 C 更受限制的理论计算机来计算复杂度。
我考虑使用 not 作弊。如果您想看到真正的“作弊”,只需将所有元素放入std::set
。结果集就是您的答案。
【参考方案1】:
我不知道作弊,但memmove
基本上做了你的循环所做的事情,只是效率更高。
此外,它是最基本的实用功能之一,所以我不明白你为什么不应该使用它。
至于复杂度,算法的顺序不会改变,只是会更快。 memmove
是在汇编中实现的,并试图充分利用对齐来逐字复制而不是逐字节复制。
编辑:
好吧,在某些情况下,手动复制的指令可能比对 memmove
的调用短一些指令,但是如果您开始在内存中移动数据,那么您所做的操作本身就成本高昂,因此需要优化从大局来看,几个 CPU 周期的差距不会产生任何影响。
如果您的设计涉及性能关键数据的就地移动,您最好更改底层数据结构以完全避免复制(列表、树、哈希表等)。
【讨论】:
【参考方案2】:您的程序使用 memmove 可能会运行得更快,就挂钟时间而言,但您的 for 循环基本上可以实现相同的效果。算法复杂度不会改变。
【讨论】:
否则它可能会运行得更慢。除了测试编译器、编译器优化设置和硬件的特定组合之外,没有简单的方法可以判断。【参考方案3】:从功能上讲,memmove 与您的循环完全一样。但是,编译器和/或 C 运行时可以使用更高效的机器代码来实现 memmove。一些架构具有专门的指令,这些指令将完全在单个指令中执行此类操作。它也可以被编译器内联。
它不会改变算法的复杂性,它只是让它更快地完成。
至于在你的作业中是否会被视为作弊 - 这当然是你的教授的问题?这里没有人能告诉你。
【讨论】:
“但是,编译器和/或 C 运行时可以使用更高效的机器代码来实现 memmove。” 或者它可以使用效率低得多的机器代码来实现它。例如,在某些平台上,使用整数这一事实可以使移动操作更快(因为它们满足某些对齐要求,任意指针可能不会)但是如果您只是将void *
传递给memmove
,则该信息会丢失。
@DavidSchwartz 通常memmove
原语会检查指针对齐,并尽最大努力使用机器字作为缓冲区来最小化实际的 RAM 读/写访问。只有第一个前导/尾随字节将被非字访问复制。寄存器字节移位将用于补偿 src/dst 未对齐。
@kuroineko 是的,但这意味着如果您没有通过强制将指针转换为void *
来隐藏指针的类型,则不需要这些额外检查。编译器拥有的信息越多,它的优化效果就越好。使用memmove
而不是简单的复制循环会向编译器隐藏信息,至少在某些现实世界的实现中是这样。
@DavidSchwartz 我在 90 年代中期玩过很多自定义复制例程,我的结论是,在这个游戏中,你往往会产生效率低下的代码来试图击败编译器。对于给定的编译器和优化级别,您可能获得微不足道的收益,但所需的代码混淆通常不值得付出努力。如果你调用memmove
百万次来晃动一些字节,那么就是设计有问题,恕我直言。
@kuroineko 我同意。我只是说调用memmove
会比复制循环更好的幼稚假设是无稽之谈。实际上,现代编译器确实可以通过简单的复制循环看到正确的内容,并毫无问题地对其进行优化。如果您调用memmove
,即使您转换为void *
会隐藏它,您也需要编译器来理解对齐方式,否则几乎可以保证您的代码效率低下。【参考方案4】:
如果有多个重复项,您可以使用辅助存储非重复项:-
int aux[arr_size],cnt=0;
for(int i=0;i<arr_size;i++)
if(arr[i]!=dup)
aux[cnt++] = arr[i];
如果您需要就地:-
int i = dup_index;
int j = dup_index+1;
int dup = arr[i];
while(j<arr_size)
if(arr[j]!=dup)
arr[i++] = arr[j];
j++;
【讨论】:
【参考方案5】:库函数(如memmove(3)
)经过精心优化。如果你看看你最喜欢的编译器如何将它写成各种长度的汇编语言(以常量的形式给出),你可能会大吃一惊。
也就是说,将 $n$ 个字节从一个地方复制到另一个地方将花费时间 $\Theta(n)$,除非您知道一些有助于避免部分混洗数据的东西,而不仅仅是将其减少一个固定的分数。
【讨论】:
以上是关于memmove 是不是移动元素(与 for 循环相同),还是一次抓取整个内存块?的主要内容,如果未能解决你的问题,请参考以下文章
C memmove相当于for loop - segfault