標籤:

為什麼不都用memmove代替memcpy?

memmove相比memcpy增加了內存重疊的判斷,更加安全,效率只是差了那麼一丟丟, 為什麼經常看見memcpy, 很少看見memmove 呢 ?是不是因為memcpy語義上說明了兩段內存是不重疊的所以有些場景使用memcpy 更合適 ?


不要嘲諷題主,其實某天很憤怒的Linus也是這麼想的——

Strange sound on mp3 flash website

Linus Torvalds 2010-11-30 20:50:25 EST

(In reply to comment #128)

&>

&> In Adobe"s software.

&>

&> &> I"m no great fan of flash but it"s an essential part of life on the web these

&> &> days and I had thought that the Fedora project had finally put its days of

&> &> broken flash support behind it.

&>

&> Fedora"s flash support is fine. Adobe"s software is broken.

Quite frankly, I find your attitude to be annoying and downright stupid.

How hard can it be to understand the following simple sentence:

THE USER DOESN"T CARE.

Pushing the blame around doesn"t help anybody. The only thing that helps is Fedora being helpful, not being obstinate.

Also, the fact is, that from a QA standpoint, a memcpy() that "just does the right thing" is simply _better_. Quoting standards is just stupid, when there"s two simple choices: "it works" or "it doesn"t work because bugs happen".

Standards are paper. I use paper to wipe my butt every day. That"s how much that paper is worth.

Reality is what matters. When glibc changed memcpy, it created problems. Saying "not my problem" is irresponsible when it hurts users.

And pointing fingers at Adobe and blaming them for creating bad software is _doubly_ irresponsible if you are then not willing to set a higher standard for your own project. And "not my problem" is not a higher standard.

So please just fix it.

The easy and technically nice solution is to just say "we"ll alias memcpy to memmove - good software should never notice, and it helps bad software and a known problem".

(加粗是我加的)

當然他這是從庫函數的角度來說,他覺得從一開始就乾脆搞成memcpy就是memmove,然後就沒這麼多毛病了。

另外有人質疑說到底性能差多少。Linus的Argument是memmove就比memcpy多一條判斷指令。

我來換句話說,如果反正地址是不重疊的,那麼memmove一定可以寫成if (地址不重疊) memcpy();的形式。而如果地址是重疊的,速度慢一點的defined behavior總比undefined behavior強。


當 memcpy / memmove 優化到極致,多一兩次判斷對整體性能的影響都是比較大的,特別是再流水線比較長的處理器體系中,即便就是有多次判斷,一般也要想各種辦法歸併,比如 memmove 開頭需要用兩個判斷來比較是否有覆蓋,類似這樣:

if (dst &< src || dst &> src + size) { 正向拷貝 } else { 逆向拷貝 }

用位運算,可以將兩次判斷合併成一次判斷:

if (((dst - src) | (src + size - dst)) 0x80000000) { 正向拷貝 } else { 逆向拷貝 }

這樣分支少了一半,推而廣之,連續的大小比較可以歸納為一次比較:

if (x &> a || y &> b || z &> c || w &> d) { ... }

可以變換成:

if (((a - x) | (b - y) | (c - z) | (d - w)) &< 0) { ... }

或者

if (((a - x) | (b - y) | (c - z) | (d - w)) 0x80000000) { ... }

用位運算將四次比較合併成了一次。

類似的還有,兩次判斷

if (x &>= 0 x &< limit) { ... }

合併成:

if ((unsigned)x &< limit) { ... }

以及四次

if (x &< 0 || x &>= w || y &< 0 || y &>= h) { ... }

也可以變成

if (((x) | (w - 1 - x) | (y) | (h - 1 - y)) &< 0) { ... }

四歸一

---

不過 c 標準庫里冗餘挺多的,有些也不一定是性能問題,還有歷史遺留原因。


我建議我們討論的時候,show the code,而不是空口從已知的實現上猜測

隨便舉個例子

http://quick-bench.com/gMd_GoRdy4LaL4gWMYLGIvR2t_4

http://quick-bench.com/KHKhJ-1MYCW8tR0VZeO9Un9-g30

橫坐標是拷貝的內存大小,從結果上來看,memmove還稍微快一點

從googlebench上抄來的測試


沒有必要,99.99%的情況下memcpy的兩個對象都是不可能重疊的,memmove反而是少數情況,比如要移動下基本類型數組元素之類的,這種以前也從來沒用過memcpy。更複雜的情況下還涉及到拷貝/移動構造等,本來也用不成memcpy


就是歷史包袱。

題主全用 memmove 代替 memcpy 的想法,不僅不可笑,而且如果放到現在來設計標準庫,只提供一個函數才是正確的設計。

舉證幾點:

  1. Linus 曾經談過這個話題, @楊個毛 的答案中已經完整引述了。
  2. Windows 的 C 運行庫(msvcrt)里,memcpy 實際上是 memmove 的 alias。微軟雖然在 MSDN 里還是按照標準的說法描述 memcpy 和 memmove,但它的 C 庫實際上二十年前就不區分了。
  3. 【這一條有誤,刪除】

有其他答主認為:

因為實際情況中,兩個區域是否重疊往往是可以預期的

真的未必。Linus 說的那一大段,當時背景就是 adobe flash player 里有一些該使用 memmove 的地方誤用了 memcpy,glibc 某一次升級後暴露了 flash 的這個問題,導致 flash 在 Linux 下面播放音頻有雜音。

此時讓程序崩潰掉反而是好事,因為崩潰能夠更明顯的提醒你這裡有個bug

memmove 誤用 memcpy 不一定會崩掉,可能只會讓複製結果不正確。【評論區提示 OpenBSD 的 memcpy 在重疊時會崩】。

關於效率,也就是 memmove 開頭加一個分支,不重疊時走 memcpy 一樣的代碼。調用一次函數比那個分支貴多了,真在極端情況下要省一兩個 cycle,也應該先考慮內聯。

另,現在很多 Linux 發行版已經在 gcc 中默認把 _FORTIFY_SOURCE 給打開了,它給很多函數增加額外的安全檢查,例如 memcpy(dst, src, n) 會被替換成 __memcpy_chk(dst, __builtin_object_size(dst), src, n),後者會增加對緩衝區大小的檢查;有的發行版還把 -fstack-protector-all 也給默認打開了。可以看出,大家現在已經不再認為這些簡單的檢查會有什麼效率問題。

C 的歷史太久,不要覺得它的設計都是對的。再舉個例子,time 函數大家都熟悉:

time_t time(time_t *ptr);

為什麼它既把時間寫進 *ptr,又作為返回值返回呢?見過有人一本正經地論證這個設計有多麼牛逼…… 其實哪有那麼複雜啊,就是因為 time_t 最初是一個結構體,當時是 void time(time_t *ptr),後來改成整數了,加上返回值方便一點,又為減少對舊代碼的影響沒有刪掉那個參數。

又比如 | 優先順序比 == 低,不要試圖去認證它有多合理,它就是不合理。這麼設計,純粹是因為最早的 C 沒有 ||,當時邏輯與、或也用 |(對,沒有短路特性)遺留下來的。


因為實際情況中,兩個區域是否重疊往往是可以預期的。如果真重疊了,應該是程序bug,此時讓程序崩潰掉反而是好事,因為崩潰能夠更明顯的提醒你這裡有個bug,更有效率的定位問題。

所以,某種程度上,可以把memcpy當做一種斷言,出現memcpy意味著程序員斷言兩個地址在指定尺寸內不重疊。


你是否能接受在某些情況下,強制犧牲性能以減少失誤風險?

不管你能不能,我是希望你不要強制到我頭上。


看了題主的問題和其他答主的回答,覺得這個問題挺好的,在用Clang AddressSanitizer之前,我也不知道還有memcpy和memmove的差異,甚至可能很多人聽都沒聽過memmove。

AddressSanitizer有一個庫函數檢測就是檢測是否在有地址重疊的情況下誤用了memcpy,如果懂得跑AddressSanitizer的話,不煩跑一跑再改。至於跑應用的話,確實memcpy批量替換成memmove最簡單,心智負擔最輕。那一句條件判斷的性能差異,平常是可以忽略不計。


因為內存複製的使用比你想像中要頻繁,幾個位元組都用memcpy。所以多一條少一條都會影響總體表現。

譬如:

匿名用戶:熟練使用 C / C++ 各種 undefined behaviour 是什麼樣一種體驗?


絕大多數人是不知道的吧,說省幾個周期的,要真想省幾個周期,就不會用C語言了,多幾個周期,比出問題焦頭爛額划算多了,然而,我們用的是memcpy,但是拷貝完之後在memcmp一下,我們不僅要防重疊,還要防內存錯,逃...


因為它「考慮了」有重疊,這不應該是程序員考慮的嗎?讓我想起來c的變長數組,為什麼很少有人用,動態數組多智能啊,用整數變數初始化數組長度,殊不知,這違背了ccpp的語言的原則和優勢


因為很少人知道這些細微差別,而且一般不會傳入內存重疊的指針。


先問是不是,再問為什麼。我只說arm64/aarch64平台。

glibc是沒有區別。

測試的時候,發現瓶頸在CPU的unalign access性能是否如同說明書一般沒有penalty。

(注意看下圖的9-11個位元組)

或者看看我的博客

聽說你想用Go彙編寫memmove? - zhuo/blog

和我最後提交的patch

https://go-review.googlesource.com/c/go/+/83175


關鍵是我在Windows VS2012上測試,memmove在內存重疊的時候會崩潰,memcpy也會.我之前的項目就遇到這個問題,最後還是自己判斷複製的。


「只差了那麼一丟丟」,具體是多少呢?題主能上個實驗結果嗎?針對不同尺寸的內存塊的複製比較


沒想過這事。一直用memcpy。大家都用。


推薦閱讀:

反編譯工具能反編譯出注釋嗎?
程序的靜態存儲區,動態存儲區和堆以及棧的關係是什麼?
為什麼很多編程語言採用花括弧區分 block 而非縮進?
為什麼說 C 語言是系統級編程的首選?
Linux 下有沒有像 Visual Studio 一樣的,有自動填充、提示語法錯誤、斷點調試等功能的 C/C++ IDE?

TAG:C編程語言 | CC |