« lua 的 table 处理 | 返回首页 | 实现一个虚拟机 »

VC 对 memcpy 的优化

在很多编译器中,memcpy 是一个 intrinsic 函数,也就是说,这个函数是由编译器实现的。它比 inline 函数更容易被编译时优化。编译器可以根据 memcpy 的参数是常量还是变量做出多种版本,达到最佳的性能。这一点,用 inline 或者 template 的技巧都无法办到。

我们看看 VC 对 memcpy 的优化。(所用版本 VC6)

void foo(void *d,const void *s)
{
memcpy(d,s,1);
}

选性能最优化,生成汇编代码可以看到。代码被优化成:

mov eax, DWORD PTR _s$[esp-4]
mov edx, DWORD PTR _d$[esp-4]
mov cl, BYTE PTR [eax]
mov BYTE PTR [edx], cl

只是一个字节拷贝,用 cl 寄存器 mov 完成的。

把 1 改成 4 后:

mov eax, DWORD PTR _s$[esp-4]
mov edx, DWORD PTR _d$[esp-4]
mov ecx, DWORD PTR [eax]
mov DWORD PTR [edx], ecx

就变成了一条最普通的 mov 指令。

如果是 8 个字节:

mov eax, DWORD PTR _s$[esp-4]
mov ecx, DWORD PTR _d$[esp-4]
mov edx, DWORD PTR [eax]
mov DWORD PTR [ecx], edx
mov eax, DWORD PTR [eax+4]
mov DWORD PTR [ecx+4], eax

就是两条 mov 指令。

直到长度是常量 19 还是用 mov 完成的:

mov eax, DWORD PTR _s$[esp-4]
mov ecx, DWORD PTR _d$[esp-4]
mov edx, DWORD PTR [eax]
mov DWORD PTR [ecx], edx
mov edx, DWORD PTR [eax+4]
mov DWORD PTR [ecx+4], edx
mov edx, DWORD PTR [eax+8]
mov DWORD PTR [ecx+8], edx
mov edx, DWORD PTR [eax+12]
mov DWORD PTR [ecx+12], edx
mov dx, WORD PTR [eax+16]
mov WORD PTR [ecx+16], dx
mov al, BYTE PTR [eax+18]
mov BYTE PTR [ecx+18], al

长度达到 20 后,就转变成了使用 rep movsd

push esi
mov esi, DWORD PTR _s$[esp]
push edi
mov edi, DWORD PTR _d$[esp+4]
mov ecx, 5<strong>
rep movsd</strong>
pop edi
pop esi

如果长度并非 4 的整数倍的话,比如复制 23 个字节:

push esi
mov esi, DWORD PTR _s$[esp]
push edi
mov edi, DWORD PTR _d$[esp+4]
mov ecx, 5
rep movsd<strong>
movsw
movsb</strong>
pop edi
pop esi

编译器会在后面插入 movsw 和 movsb 。

现在我们来看看,memcpy 的长度是变量的情况:

void foo(void *d,const void *s,size_t size)
{
memcpy(d,s,size);
}

这次编译器直接调用了 rep movsd

mov ecx, DWORD PTR _size$[esp-4]
push esi
mov esi, DWORD PTR _s$[esp]
mov eax, ecx
push edi
mov edi, DWORD PTR _d$[esp+4]
shr ecx, 2
rep movsd
mov ecx, eax
and ecx, 3
rep movsb
pop edi
pop esi

因为我们并不知道 size 是否是 4 的整数倍,所以尾巴上用 and ecx,3 / repmovsb 来处理了一下。

那么我们能否通知编译器,需要 memcpy 的数据块长度是 4 的倍数呢?答案是可以的。看看编译器怎么编译 memcpy(d,s,size*4);

mov ecx, DWORD PTR _size$[esp-4]
push esi
mov esi, DWORD PTR _s$[esp]
push edi
mov edi, DWORD PTR _d$[esp+4]
rep movsd
pop edi
pop esi

非常简洁,不是吗?

Comments

我上个回复字都打错了,看样子得多学习了,哎,可悲啊

辛苦了,www.wangyezhizuo.org 我就搞不懂这么多,云风你既知道做网站有知道这些东西真是了不起,佩服、佩服。

又看了一遍回复,发现到了后来进入了名词之争,大家的意思没有什么本质区别。

学术争论跟修养两码事,毋须过于神话了修养,至于一些朋友把修养当作一种信仰,那是个人喜好了。coder自己都没通看所有留言,就把问题引申到修养上,这未必就是修养。匿名兄太过激动附带“人身攻击”了。

恩. 都是些小问题. 都大度点吧. 谁开个头. 有什么意义呢这样子. 谁对谁错都要有点承受力嘛.最后,这不过是个memcpy的讨论.还有更多更重要,重要上100倍,1000倍的事等着你们做呢.

R U UNDERSTAND?
1.你明白吗?
2.你懂不?

好像差很远啊!
风云,砍!

搜memcpy搜到这里,忍不住说下那个匿名的同学,自己首先态度不好,然后还在那“教育和人性。。。”。
建议此同学好好学习一下老罗的装逼犯教程。DO u understand?

本来看了本篇文章,心情挺好。结果看了这些许的争论,有点郁闷。匿名老兄归根结底会这么火气大,说出 R U understand之类的话,不也是以一种恃才傲物的表现吗?
什么话不能好好说?!!!
还有大家不要动辄上升到中国教育诸如此类的程度上探讨问题。
我一个外人都觉得窝火,何况主人乎?

很好的文章,现我有个问题大家帮我讨论下:
对一段字符串是中英字混合的,以16位编码保存。
另一段字符串是全英文,以8位编码的。使用MEMCPY(,,)拷贝到另一段内存中对中英字混合的内存块拷贝不全。碰到英文编码的就结束

看了这里的评论。以及中国技术论坛、Blog的现状。感触颇深,往往很好的技术辩论到最后都弄得不太愉快,争论已经变质。我觉得其根本可能是高手之间太在乎输赢了,其实本来无一物,看清得失,才能走的更远。记得Abrash的Black Book中有一段提及有人找他说Mode X不是他首先发现的而是她老公。其实有什么所谓。编程是实践学科,很少有人真正创造什么,大家都清楚Carmack DOOM中的BSP算法七十年代就有了。从中得到乐趣才是真的,愿大家都成为Grand Master。顺便赞一下,云风的书不错。

言归正传,这个问题我在gcc下面简单测试了一下,有两个疑问:
1 为什么会对memcpy进行优化而不会对memmove进行优化
2 gcc的行为是以size == 64 bytes为界,达到64的时候call memcpy,63以及以下,就优化掉了
gcc -O3的优化

那位匿名老兄强调的人文精神是正确的,不过你在整个讨论过程中是否也犯了同样的错误呢?的确云风的口气你可能不适,但这里只是个人的 blog,说句不好听的,就算是云风骂了你又能怎样?大家只是讨论问题,不希望看到这种无谓的讨论,也希望云风不要再浪费时间作过多地说明,每个人都有自己的风格和特色。不过,作为blog的主人,云风还是要有些大家风范的。

vc8 将 4096 作为一个分界线是有道理的。这个数字是个经验数字,究其原理是因为 P4 的 L1 Cache 有 8k, 4k 是 8k 的一半。当数据全部工作在 L1 Cache 中时,任何企图做的优化都会因为代码复杂度上升而被抵消掉。

又多了几篇帖,对我的观点又提供了几个清晰的例证,尤其是关于教育和人性的关系方面的。我不会再做任何回复。一是到此为止,我的一切看法均有大家回帖为证,在这里所有人思考问题和对待事物的方法旁观者自能看懂。二是人性丑陋的一面已开始由台后走向台前,而执迷不悟是丑陋人性的基本属性,因此继续置身其中在我看来是个笑话。我希望,随着中国快速的发展,人文环境和教育体制也同步进步,尽快终止像云风这样以人的标准来衡量是次品或废品的中国技工的产生。也许这需要一代或两代人的时间,但是历史终将翻过这一页,中国将更加美好,世界将更加美好,技术生活将更加美好。

抱歉,因为记忆错误,前后有点不一致。对于字节来说,4096是VC8是否调用函数的阀值,不知道是否是开发人员随意指定的。那个1024是记忆错误(记成4的倍数了)。

换了个 markdown 的插件,看看 0<1 转换是不是正常。

原来是小于号被当作html了。我的留言是:在VC8下,常数小于24时会展开(23是rep),小于1024时会使用rep指令。而变量会被处理成函数调用,即使是给出明显的提示,比如size*4.

呵呵,你这里我是经常来看,你能经常花时间整理一些东西,不管大也好小也好,总之都给大家带来一些收获。大部分你说的一些技术问题我都有看也比较认同,有些问题觉得需要讨论下我就留言一下。就像去看新浪一样,隔两天总会看看你这里有什么新内容,算是一种习惯吧。看来以后还是少说多看为好,勿怪勿怪。

"让你去看看vc8的memcpy的汇编实现本来就是一个好建议" 前面的留言并没有提出这个建议,“关于 memcpy ,看生成的汇编即可。”这句话是我自己说的。

我从一开始就没认为这篇 blog 有什么研究成果在里面,只是把 VC 生成的汇编列出来。任何对 VC 稍有熟悉的人几分钟就能做一次,没什么技术含量。所以我说这个问题不用多探究,"看看编译器生成汇编即可"

我写 "非也,release 编译优化后,memcpy 是不会连接 crt 库的,memcpy 的实现不依赖 msvcrt" 这句是依赖本篇 blog 的前提,我在文中早以指出,所用环境是 VC6 。换个编译器自然可能不用,而且又不是什么需要辩解的理论,有什么好争辩的?

的确,就争论问题而言,的确可以在“不过,你说的编译器是vc6,我说的是vc8.微软的官方解释是,vc8的memcpy函数优于inline汇编” 打住。如果换做 coder 老兄,估计就此打住,换做以前的我,也会如此。不过现在我想法不同,随意展开一些关于 intrinsic 的概念总不是坏事,开罪一个匿名的朋友又有何妨,让诸如 coder 老兄这样的朋友误解又有何妨?如果帮关心此贴的朋友中一些对此概念还模糊的澄清技术问题不是美事?事后看来并未有错。

你觉得 "R U UNDERSTAND?看来,你只有去看看vc8生成的汇编才会弄懂。"这句话是正确的提建议的方式吗? 原本也无所谓,正好碰上那天我别的事情有点心烦,回复也就激烈了点。本来写完了我就想删掉,而后一想,本来有些不高兴,何必掩饰,装出的修养又有何用?而且下来的留言心境不同,自然也就不再如此。而之前,哪有什么不屑。我看这位 coder 老兄倒是自己的心态有问题,才会 YY 我在"觉得是人家在置疑自己的研究才会表现出有些不屑" 了 :) 诚如 coder 老兄自己所言,"网上讨论本来就没语气,如果你带着自己认为的语气去读当然会有火了呀" 我本没有不屑的语气,那个匿名老兄也觉得有。匿名老兄匿名就算了,以 coder 老兄如此之修养,读来也有,coder 老兄多次在此处留言,想必是关心在下的,在此谢过。这提高修养一事,应该你我共勉。

汗,云风你又犯戒了,哈 说实话本来这个话题可以在 “不过,你说的编译器是vc6,我说的是vc8.微软的官方解释是,vc8的memcpy函数优于inline汇编” 就打住了,两人说的东西已经不是一样的环境了。云风还是过于武断了,觉得memcpy的实现不会去用crt,人家让你去看看vc8的memcpy的汇编实现本来就是一个好建议,事实上你后来还是去了,可咋就觉得人家语气不对呢。我认为你还是心态有问题,你多少觉得是人家在置疑自己的研究才会表现出有些不屑,网上讨论本来就没语气,如果你带着自己认为的语气去读当然会有火了呀。还是我以前和你说过的人品和修养是两回事情,我觉得你需要加强自身的修养。不过你这个年龄应该正是气盛之时,也无所谓。
和你说的这些都是以朋友旁观的看法来说的,切不可带上不应有的语气哦。

我的留言被截断了,补齐
在VC8下的试验:
当size是<1023的常数时,被优化非函数调用。<24时直接展开(但23是rep)。当size是变量时,不管是否有*4的暗示,都生成函数调用。

看了下最早的回复,发现引起某位老兄反感的一句话应该是"关于 memcpy ,看生成的汇编即可。memcpy 是个 intrinsic 所以跟 inline 不同。"并且被当成“以一种比别人懂得多的口吻说话”。

我赞同不应该自以为是的口吻说话,但是这句话无论是主观上还是客观上都没有“自己懂的多的意思”。正如我所说“关于 memcpy ,看生成的汇编即可" 问题是很明了的,知道某个编译器怎样生成 memcpy 的代码无任何值得炫耀的地方。如果有人看过 vc8 的生成代码,而我没有看过,我也不会强词夺理的说他是错的。所以后半句“memcpy 是个 intrinsic 所以跟 inline 不同”并非指责谁“连intrinsic 都不懂”这个意思,更不可能嘲笑人“不懂得看编译器生成的代码”这层意思。因为 vc8 生成的代码就是那样,以此嘲笑别人就是嘲笑自己。之所以写这一句,是因为 intrinsic 的确有它的特别行为,它可以针对上下文对生成代码做出最佳选择。如果有人把它理解成起到 inline 关键字的作用就不对了。当时我不敢妄断匿名老兄是否了解这个区别,但是把这句话写到这里是无错的。

当看到大写的 "R U UNDERSTAND?看来,你只有去看看vc8生成的汇编才会弄懂。" 正好我的心情不佳,接下来的回复才言辞激烈了。而且“当你给memcpy传的长度为小常数(好像是不大于256,记不清了)时,生成的汇编码是rep movsd指令”这句话很容易让我理解为说这句话的人没有真正理解 intrinsic 的作用。因为当 memcpy 的长度参数很小时,rep movsd 显然不是最佳策略。intrinsic 正是保证编译器参考上下文的基础上生成最佳代码。什么时候编译器不能联系上下文,只能生成固定的代码;例如统一生成 rep movsd 呢?那就是为 memcpy 写一个 inline 函数。所以我即使到现在都不敢肯定这个匿名老兄是否真正理解 intrinsic 的作用。只是别人是否理解关我何事,我在我的 blog 上把技术问题阐述清楚不就够了。

这么简单就认为被人看小了,是不是应该检讨自己,自负过头就有点自卑了?

由于受到成长环境和教育经历的限制,很多人并不能觉察自己在以一种比别人懂得多的口吻说话,更不能觉察这样做有什么不好。这种情况很普遍,尤其在以培养工具为己任的中国教育体制下。但是请记住,施与即得到。

又看了一遍回复,发现到了后来进入了名词之争,大家的意思没有什么本质区别。

intrinsic的意义,我是同意cloud的,也就是说属于编译器内置的函数,由编译器负责解释,当然这都是在intrinsic选项打开的情况,vc8是默认关闭的,但并不说明vc8会认为intrinsic直接调用函数更高效,而是根据具体的情况来生成指令,我简单试验了一下,设置/Oi后,对于<4096的常量size,总是优化成指令的。在<=24的时候,直接展开成为指令(23是rep),而变量都会调用memcpy。即使是size*4。

这个问题本身我想没有多少异意,所以我不想再就这个问题争执下去。我想 msdn 上既然都写了,大家都看到,就算以前没看,因为这个争执,有兴趣的人肯定也看了。前面 Bennie 贴的 msdn 的原文,我自然也不可能不去看一遍。虽然我的英文不好,但是我想意思还是能理解的,也不会不理解 inline 这个单词。

从最初的回复,我都没有恶意,如果被前面的匿名老兄误解,深表歉意。这里对前面的回复稍做解释,并非解释技术点,而是表达前面的文字中没有恶意。

之所以一开始提出 intrinsic 和 inline 的不同。是因为编译器对其实现的不同。 inline 的东西是依据 C 本身的语法,或者编译器扩展的语法写成的,而 intrinsic 则是跟上下文有关系。内部的实现不得而知。所以memcpy 才可以做到根据传入常数的不同生成不同的汇编代码。甚至在传入变量时,依据编译器对变量的判定做出优化(比如VC6 中对 4 的倍数的优化)。正是这样,编译器才不会表现出对一切小的常数皆用统一的 rep movsd 的形式解决。

之所以我曾经回复说" memcpy 的实现看编译器生成的汇编即可" 本意就是说,编译器怎样生成 memcpy 的对应代码,本就摆在那里,一看便知。vc6 怎样实现,vc8 怎样实现,或者 gcc 怎样实现,icc 又怎样优化,不会因为我们在这里讨论而有改变,所以没有什么需要争论的东西。每个编译器自然有它自己的解决方法。没有任何贬低匿名老兄学识的意思。

我想我写这篇 blog 只想说明问题,而回复,是借留言展开问题。没有必要去和一篇留言咬文嚼字为动机去证明一个匿名的人的错误。

在我这里来证明自己什么都懂又有何意义呢?我勿须关心一个素不相识的人读过 msdn 没有,是不是会写程序,能不能读懂 x86 的汇编。

无论如何,如果发言无意刺痛了他人的神经(我想可能是有的,没有人会无来由的恶语相向)道歉是应该的,另外感谢其让我安装了 VC8 并从同事那了解到 VC8 现在是免费的,可以任意使用。

回下面一个朋友的:movsd 可以讲 [esi] 里的一个 dword 赋值给 [edi] 并对 esi,edi 寄存器做相应的调整。而 rep 前缀将这个操作重复 ecx 次。 rep movsd 一般用于复制内存块。精确的定义可以去 intel 网站上查。

在网络上讨论技术问题,目的不是证明别人是错误的,而是为了自身的提高。此外,也不要轻易的对人做价值判断。
VC8对Compiler Intrinsics的说明:
If a function is an intrinsic, the code for that function is usually inserted inline, avoiding the overhead of a function call and allowing highly efficient machine instructions to be emitted for that function. An intrinsic is often faster than the equivalent inline assembly, because the optimizer has a built-in knowledge of how many intrinsics behave, so some optimizations can be available that are not available when inline assembly is used. Also, the optimizer can expand the intrinsic differently, align buffers differently, or make other adjustments depending on the context and arguments of the call.

这是VC8的说明,此外,那个256肯定是不正确的,文档中没有说明,而且我自己试验的结果也证明是不对的。

这个问题我支持Anonymous,我觉得云风的确是在曲解别人的意思

请问cloud, rep movsd指令是做什么的?页复制吗?

到现在你还是不懂intrinsic之context下的inline是什么意思。这不算太奇怪,就像一个老外搞不懂中文“走”有步行和离开的意思也不奇怪一样,但是如果这个老外自以为是地去普及中文知识,说“暴走北京”就是“离开北京”的意思,那就完全是属于理解能力和心态问题了。文字已展示得很清楚,再辩解只会越描越黑。

写完前两篇回复后,觉得自己挺无聊的。善意的理解,前面的匿名老兄开始并无恶意。不过不知为何火气这么大,以至于做人身攻击。

芝麻点大的问题需要吵么,问题本身也不见分歧。以下不再咬文嚼字,这篇 blog 的原意只是列举编译器对 memcpy 的优化策略。

最后,普及一下基础知识。intrinsic 并非“在需要的时候将函数转化为inline汇编”,这只是一种表现形式。intrinsic 可视为编译器对 C/C++ 的一种扩展,由编译器以一种关键字的处理方式来决定 intrinsic 的实现,而不是直接靠链接器做函数的链接。

我只是帮助你纠正你的错误,"当你给memcpy传的长度为小常数(好像是不大于256,记不清了)时,生成的汇编码是rep movsd指令"

无论是 vc 的那个版本,memcpy 都不只生成 rep movesd 指令。不知道你从一开始的第一篇发言到最近的一篇到底想说明什么问题?证明我写的是错的?没写错啊?还是只是想强调"vc8 的行为有了改变"? 一开始注明了 vc8 吗? 亦或是逞口舌之利,用诸如"低下的理解力,扭曲的心态"这样的词做人身攻击? 抱着什么心态讨论问题,是不用文字辩解的。

我说了我指的是vc8而你说的是vc6,并没有否定你对vc6的有关说法。本来这个题目既然是关于编译器和memcpy的,我只是想指明一下vc8现在的优化方式有了改变,和原来vc6不一样了。没想到你想当然以为别人不懂指令的优化,不懂intrinsic,也不会去看汇编码,最后为了文过饰非,竟然把inline和call的分界偷换为rep movsd和其他指令的分界。其实真正对什么都是一知半解,需要各方面学习提高的正是你自己。你之所以会表现得这样无知和咄咄逼人,完全是你低下的理解能力和扭曲的心态造成的。

刚才从 microsoft.com 当了 vc2005 装上看了一下,memcpy 的 crt 实现会根据实际的机器,启用诸如 SSE 的版本。

关于这样做在什么时候高效,什么时候低效,不是这篇 blog 的讨论范畴。

这篇文章所讨论的 VC 对 memcpy 的行为,在篇头已经列出,所用环境为 VC6

生成代码的效率怎样比较高,跟 cpu 有关。每种编译器自然有它的策略,vc8 我没有装过,所以请你自己去检查是否真是如此。但是,如果 memcpy 的长度参数是一变量,且数字不大的时候,如何编写 memcpy 函数,那一次 call 的调用消耗是无论如何抵消不了的。

其实,memcpy 在 VC6 上的行为,我上面已经列举的很清楚了。显然你没有看,才写的出 "如果你将intrinsic打开,当你给memcpy传的长度为小常数(好像是不大于256,记不清了)时,生成的汇编码是rep movsd指令,其他任何情况生成的永远都是call memcpy" 这样的话。如果 vc8 连 memcpy (d,s,4) 这样的代码都编译成 rep movsd 那真是一种倒退。至于什么是 intrinsic ,还是回去多读读书。下次不要在这里咄咄逼人的发言。

intrinsic 的作用就是需要的时候将函数转化为inline汇编,这里的inline指的是英文本意的inline,即直接生成汇编代码,与vc里面那个叫inline的macro不是一回事.微软的vc8说明里用的就是这个词。在vc8里,如果你将intrinsic关闭,得到的汇编码永远是call memcpy,任何情况都不会有rep movsd。如果你将intrinsic打开,当你给memcpy传的长度为小常数(好像是不大于256,记不清了)时,生成的汇编码是rep movsd指令,其他任何情况生成的永远都是call memcpy。R U UNDERSTAND?看来,你只有去看看vc8生成的汇编才会弄懂。

关于 memcpy ,看生成的汇编即可。memcpy 是个 intrinsic 所以跟 inline 不同。

非非也,编译器认为最优的性能就是调用memcpy函数,而不是将其转换为inline汇编指令,除非长度是一个很小的常数。不过,你说的编译器是vc6,我说的是vc8.微软的官方解释是,vc8的memcpy函数优于inline汇编。

非也,release 编译优化后,memcpy 是不会连接 crt 库的,memcpy 的实现不依赖 msvcrt

只有当长度参数为常数并且小于一定数值时,vc才会将memcpy优化成一段代码,其他任何情况都会调用msvcrt里面的memcpy.

sorry 我找就放弃用 qq 了, 还是 email 吧,给我点喘息的时间。上班的时候不想因为 IM 把思路打断。

云风你好。能否认识一下,我QQ号9968858

最后一段,还是有参考价值的。谢过,呵呵。

很好的调查!

还可以进一步研究一下Intel C++的memcpy,看看它是否用了MMX指令。

Post a comment

非这个主题相关的留言请到:留言本