Memmove neden memcpy'den daha hızlıdır?


90

Zamanının% 50'sini memmove'da geçiren bir uygulamada performans sıcak noktalarını araştırıyorum (3). Uygulama, sıralanmış dizilere milyonlarca 4 baytlık tamsayı ekler ve eklenen değere yer açmak için verileri "sağa" kaydırmak için memmove kullanır.

Beklentim, hafıza kopyalamanın son derece hızlı olmasıydı ve memmove'da bu kadar çok zaman harcanmasına şaşırdım. Ama sonra memmove'nin yavaş olduğu fikrine kapıldım çünkü büyük bellek sayfalarını kopyalamak yerine, sıkı bir döngü içinde uygulanması gereken üst üste binen bölgeleri hareket ettiriyor. Memcpy ile memmove arasında bir performans farkı olup olmadığını öğrenmek için küçük bir mikro ölçüt yazdım ve memcpy'nin ellerini aşağı çekmesini bekledim.

Karşılaştırmamı iki makinede (çekirdek i5, çekirdek i7) çalıştırdım ve memmove'un aslında memcpy'den daha hızlı olduğunu gördüm, eski i7'de neredeyse iki kat daha hızlı! Şimdi açıklamalar arıyorum.

İşte kıyaslamam. Memcpy ile 100 mb kopyalar ve ardından memmove ile yaklaşık 100 mb hareket eder; kaynak ve hedef çakışıyor. Kaynak ve hedef için çeşitli "mesafeler" denenir. Her test 10 kez çalıştırılır, ortalama süre yazdırılır.

https://gist.github.com/cruppstahl/78a57cdf937bca3d062c

İşte Core i5 (Linux 3.5.0-54-generic # 81 ~ exact1-Ubuntu SMP x86_64 GNU / Linux, gcc 4.6.3'tür (Ubuntu / Linaro 4.6.3-1ubuntu5). Parantez içindeki sayı kaynak ve hedef arasındaki mesafe (boşluk boyutu):

memcpy        0.0140074
memmove (002) 0.0106168
memmove (004) 0.01065
memmove (008) 0.0107917
memmove (016) 0.0107319
memmove (032) 0.0106724
memmove (064) 0.0106821
memmove (128) 0.0110633

Memmove, arkadan öne doğru kopyalayan, SSE optimize edilmiş bir montajcı kodu olarak uygulanır. Verileri önbelleğe yüklemek için donanım ön belleğini kullanır ve 128 baytı XMM kayıtlarına kopyalar, ardından bunları hedefte depolar.

( memcpy-ssse3-back.S , satırlar 1650 ff)

L(gobble_ll_loop):
    prefetchnta -0x1c0(%rsi)
    prefetchnta -0x280(%rsi)
    prefetchnta -0x1c0(%rdi)
    prefetchnta -0x280(%rdi)
    sub $0x80, %rdx
    movdqu  -0x10(%rsi), %xmm1
    movdqu  -0x20(%rsi), %xmm2
    movdqu  -0x30(%rsi), %xmm3
    movdqu  -0x40(%rsi), %xmm4
    movdqu  -0x50(%rsi), %xmm5
    movdqu  -0x60(%rsi), %xmm6
    movdqu  -0x70(%rsi), %xmm7
    movdqu  -0x80(%rsi), %xmm8
    movdqa  %xmm1, -0x10(%rdi)
    movdqa  %xmm2, -0x20(%rdi)
    movdqa  %xmm3, -0x30(%rdi)
    movdqa  %xmm4, -0x40(%rdi)
    movdqa  %xmm5, -0x50(%rdi)
    movdqa  %xmm6, -0x60(%rdi)
    movdqa  %xmm7, -0x70(%rdi)
    movdqa  %xmm8, -0x80(%rdi)
    lea -0x80(%rsi), %rsi
    lea -0x80(%rdi), %rdi
    jae L(gobble_ll_loop)

Memmove neden memcpy'den daha hızlı? Memcpy'nin bellek sayfalarını kopyalamasını beklerdim, bu döngüden çok daha hızlı olmalı. En kötü durumda, memcpy'nin memmove kadar hızlı olmasını beklerdim.

Not: Kodumda memmove'u memcpy ile değiştiremeyeceğimi biliyorum. Kod örneğinin C ve C ++ 'yı karıştırdığını biliyorum. Bu soru gerçekten sadece akademik amaçlar içindir.

GÜNCELLEME 1

Çeşitli cevaplara dayanarak testlerin bazı varyasyonlarını çalıştırdım.

  1. Memcpy'yi iki kez çalıştırırken, ikinci çalıştırma ilkinden daha hızlıdır.
  2. Memcpy ( memset(b2, 0, BUFFERSIZE...)) 'nin hedef tamponuna "dokunduğunuzda", memcpy'nin ilk çalıştırması da daha hızlıdır.
  3. memcpy hala memmove'dan biraz daha yavaştır.

Sonuçlar burada:

memcpy        0.0118526
memcpy        0.0119105
memmove (002) 0.0108151
memmove (004) 0.0107122
memmove (008) 0.0107262
memmove (016) 0.0108555
memmove (032) 0.0107171
memmove (064) 0.0106437
memmove (128) 0.0106648

Sonucum: @Oliver Charlesworth'un bir yorumuna dayanarak, işletim sistemi, memcpy hedef arabelleğine ilk kez erişilir erişilmez fiziksel belleği işlemelidir (birisi bunu nasıl "kanıtlayacağını" biliyorsa, lütfen bir yanıt ekleyin! ). Ek olarak, @Mats Petersson'ın dediği gibi, memmove, memcpy'den daha kolay önbellek dostudur.

Tüm harika cevaplar ve yorumlar için teşekkürler!


2
Memmove koduna baktın, memcpy koduna da baktın mı?
Oliver Charlesworth

9
Beklentim, bellek kopyalamanın son derece hızlı olmasıydı - yalnızca bellek L1 önbelleğinde olduğunda. Veriler önbelleğe sığmadığında, kopyalama performansınız azalır.
Maxim Egorushkin

1
BTW, yalnızca bir dalını kopyaladınız memmove. Bu dal, kaynak hedefle örtüştüğünde ve hedef daha düşük adreslerde olduğunda hareketi işleyemez.
Maxim Egorushkin

2
Bir Linux makinesine erişmek için zamanım olmadı, bu yüzden bu teoriyi henüz test edemiyorum. Ancak başka bir olası açıklama, aşırı yüklenmedir ; senin memcpydöngü içeriğini ilk defa b2erişildiğinde, böylece işletim sistemi olarak gider bunun için fiziksel bellek işlemek zorundadır.
Oliver Charlesworth

2
Not: Bu bir darboğazsa, yaklaşımı yeniden gözden geçiririm. Değerleri bir listeye veya ağaç yapısına (örn. İkili ağaç) koymaya ve sonra bunları sonunda bir dizi halinde okumaya ne dersiniz? Böyle bir yaklaşımdaki düğümler, havuz tahsisi için mükemmel bir aday olacaktır. Yalnızca toplu halde serbest bırakıldıklarında sonuna kadar eklenirler. Başlangıçta kaç tanesine ihtiyacınız olacağını biliyorsanız, bu özellikle doğrudur. Hızlandırma kitaplıklarında havuz ayırıcı bulunur.
Persixty

Yanıtlar:


58

Sizin memmoveiçin ise aramalar, 2 128 bayt tarafından birlikte hafızayı karıştırma olan memcpykaynak ve hedef tamamen farklıdır. Bir şekilde performans farkını açıklayan bu: aynı yere kopyalarsanız, sonuçların memcpymuhtemelen daha hızlı olduğunu göreceksiniz , örneğin ideone.com'da :

memmove (002) 0.0610362
memmove (004) 0.0554264
memmove (008) 0.0575859
memmove (016) 0.057326
memmove (032) 0.0583542
memmove (064) 0.0561934
memmove (128) 0.0549391
memcpy 0.0537919

Yine de içinde neredeyse hiçbir şey yok - zaten hafızada hatalı olan bir sayfaya geri yazmanın çok fazla etkisi olduğuna dair kanıt yok ve kesinlikle zamanın yarıya indiğini görmüyoruz ... ama memcpyelmalarla karşılaştırıldığında gereksiz yere yavaşlamanın yanlış bir şey olmadığını gösteriyor. - elmalar için.


Arabelleklerim önbelleklerden çok daha büyük olduğu için CPU önbelleklerinin fark yaratmamasını beklerdim.
cruppstahl

2
Ancak her biri aynı sayıda ana bellek erişimi gerektirir, değil mi? (Yani 100MB okuma ve 100MB yazma). Önbellek düzeni bunun etrafından dolaşmaz. Yani birinin diğerinden daha yavaş olmasının tek yolu, bazı şeylerin birden fazla kez belleğe / bellekten okunması / yazılması gerekmesidir.
Oliver Charlesworth

2
@Tony D -
Vardığım

1
Ayrıca, aynı yere kopyalayıp memcpyyine ilkini yaparsanız ne olur ?
Oliver Charlesworth

1
@OliverCharlesworth: İlk test çalıştırması her zaman önemli bir darbe alır, ancak iki memcpy testi yapmak: memcpy 0.0688002 0.0583162 | memmove 0.0577443 0.05862 0.0601029 ... bkz ideone.com/8EEAcA
Tony Delroy

27

Kullanırken memcpy, yazılanların önbelleğe gitmesi gerekir. memmoveİleriye doğru küçük bir adım kopyaladığınız yeri kullandığınızda , kopyaladığınız bellek zaten önbellekte olacaktır (çünkü 2, 4, 16 veya 128 bayt "geri" okundu). memmoveHedefin birkaç megabayt (> 4 * önbellek boyutu) olduğu bir yerde yapmayı deneyin ve benzer sonuçlar alacağınızdan şüpheleniyorum (ancak test etmekten de rahatsız olamıyorum).

Büyük bellek işlemleri yaptığınızda TÜMÜNÜN önbellek bakımı ile ilgili olduğunu garanti ederim.


+1 Sanırım bahsettiğiniz nedenlerden ötürü, geriye doğru döngü yapan bir memmove, memcpy'den daha önbellek dostudur. Ancak, memcpy testini iki kez çalıştırdığımda, ikinci çalışmanın memmove kadar hızlı olduğunu keşfettim. Neden? Tamponlar o kadar büyüktür ki, ikinci bir memcpy çalıştırması, ilk çalıştırma kadar verimsiz (önbellek açısından) olmalıdır. Yani burada performans cezasına neden olan ek faktörler var gibi görünüyor.
cruppstahl

3
Doğru koşullar verildiğinde, memcpyTLB önceden doldurulduğu için bir saniye önemli ölçüde daha hızlı olacaktır. Ayrıca, memcpy"kurtulmanız" gerekebilecek şeylerin önbelleğini bir saniye boşaltmak zorunda kalmayacaktır (kirli önbellek satırları pek çok yönden performans için "kötüdür". Ancak kesin olarak söylemek gerekirse, "mükemmel" gibi bir şey çalıştırın ve önbellek kaçırma, TLB ıskalama gibi şeyleri örnekleyin.
Mats Petersson

16

Tarihsel olarak memmove ve memcopy aynı işlevdir. Aynı şekilde çalıştılar ve aynı uygulamaya sahiptiler. Daha sonra, memkopinin örtüşen alanları belirli bir şekilde ele almak için tanımlanması gerekmediği (ve çoğu zaman olmadığı) fark edildi.

Sonuç olarak memmove, performansı etkilese bile örtüşen bölgeleri belirli bir şekilde ele alacak şekilde tanımlanmıştır. Memcopy, örtüşmeyen bölgeler için mevcut olan en iyi algoritmayı kullanmalıdır. Uygulamalar normalde hemen hemen aynıdır.

Karşılaştığınız sorun, x86 donanımının o kadar çok çeşidi olması ki, hangi bellek değiştirme yönteminin en hızlı olacağını söylemenin imkansız olmasıdır. Ve bir durumda bir sonuca sahip olduğunuzu düşünseniz bile, bellek düzeninde farklı bir "adım" a sahip olmak kadar basit bir şey, çok farklı önbellek performansına neden olabilir.

Ya gerçekte yaptığınız şeyi kıyaslayabilir ya da sorunu görmezden gelebilir ve C kütüphanesi için yapılan kıyaslamalara güvenebilirsiniz.

Düzenleme: Oh, ve son bir şey; çok fazla bellek içeriğini değiştirmek ÇOK yavaş. Tam sayılarınızı işlemek için uygulamanızın basit bir B-Tree uygulaması gibi daha hızlı çalışacağını tahmin ediyorum. (Oh sen, tamam)

Düzenleme2: Yorumlarda genişlememi özetlemek gerekirse: Mikro ölçüt burada sorun, sizin düşündüğünüz şeyi ölçmüyor. Memcpy ve memmove'a verilen görevler birbirinden önemli ölçüde farklıdır. Memcpy'ye verilen görev memmove veya memcpy ile birkaç kez tekrarlanırsa, sonuçlar, bölgeler örtüşmedikçe hangi bellek kaydırma işlevini kullandığınıza bağlı olmayacaktır.


Ama bununla ilgili - aslında yaptığım şeyi kıyaslıyorum. Bu soru, iddia ettiğiniz şeyle çelişen kıyas ölçütünün sonuçlarını yorumlamakla ilgilidir - memcpy, çakışmayan bölgeler için daha hızlıdır.
cruppstahl

Benim uygulama olan b-ağaç! Bir yaprak düğümüne tamsayılar eklendiğinde, boşluk yaratmak için memmove çağrılır. Bir veritabanı motoru üzerinde çalışıyorum.
cruppstahl

1
Bir mikro kıyaslama kullanıyorsunuz ve memcopy ile memmove'un aynı verileri değiştirmesine bile sahip değilsiniz. Başa çıkardığınız verilerin bellekte bulunduğu kesin konumlar, önbelleğe alma ve CPU'nun belleğe kaç gidiş dönüş yapması gerektiği konusunda bir fark yaratır.
user3710044

Bu cevap doğru olsa da, aslında bu durumda neden daha yavaş olduğunu açıklamıyor , aslında "daha yavaş çünkü bazı durumlarda daha yavaş olabilir" diyor.
Oliver Charlesworth

Aynı koşullar için, karşılaştırmaları kopyalamak / taşımak için aynı bellek düzeni dahil olmak üzere, uygulamalar aynı olduğu için aynı OLACAKTIR. Sorun mikro ölçütte.
user3710044

2

"memcpy memmove'dan daha etkilidir." Sizin durumunuzda, iki işlevi çalıştırırken büyük olasılıkla aynı şeyi yapmıyorsunuzdur.

Genel olarak, memmove'u yalnızca gerekiyorsa KULLANIN. Kaynak ve hedef bölgelerin üst üste binme ihtimali çok yüksek olduğunda kullanın.

Referans: https://www.youtube.com/watch?v=Yr1YnOVG-4g Dr. Jerry Cain, (Stanford Intro Systems Lecture - 7) Saat: 36:00

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.