Memcpy () ve memmove () neden işaretçi artışlarından daha hızlı?


92

Den ' pSrce N bayt kopyalıyorum pDest. Bu, tek bir döngüde yapılabilir:

for (int i = 0; i < N; i++)
    *pDest++ = *pSrc++

Bu neden memcpyveya daha yavaş memmove? Hızlandırmak için hangi numaraları kullanıyorlar?


2
Döngünüz yalnızca bir konumu kopyalar. Sanırım bir şekilde işaretçileri artırmak istedin.
Mysticial

13
Ya da benim yaptığım gibi onlar için tamir edebilirsin. Ve ve ödeyecekleri hiçbir gerçek C programcısı hiç gelen sayımlar 1için N, bu kadar hep gelen 0etmek N-1:-)
paxdiablo

6
@paxdiablo: Diziler üzerinde döngü yapıyorsanız, emin olun. Ancak 1'den N'ye döngü yapmanın iyi olduğu pek çok durum vardır. Verilerle ne yaptığınıza bağlıdır - örneğin 1'den başlayan numaralandırılmış bir liste görüntülüyorsanız, örneğin bir kullanıcıya 1'den başlamak muhtemelen daha mantıklı olacaktır. Her durumda, bunun yerine intişaretsiz bir tip size_tkullanılması gerektiğinde sayaç olarak kullanılan daha büyük sorunu görmezden gelir .
Billy ONeal

2
@paxdiablo N'den 1'e de sayabilirsiniz. Bazı işlemcilerde, azaltma sıfıra ulaştığında dallanma talimatı için uygun biti ayarlayacağından bir karşılaştırma talimatını ortadan kaldıracak.
onemasse

6
Bence sorunun dayanak noktası yanlış. Modern derleyiciler bunu memcpyveya memmove(işaretçilerin takma ad olup olmadığını anlayıp anlamadıklarına bağlı olarak) dönüştürür.
David Schwartz

Yanıtlar:


120

Memcpy bayt işaretçileri yerine kelime işaretçileri kullandığından, memcpy uygulamaları da genellikle bir seferde 128 biti karıştırmayı mümkün kılan SIMD talimatları ile yazılır .

SIMD talimatları, 16 bayta kadar uzunluktaki bir vektördeki her eleman üzerinde aynı işlemi gerçekleştirebilen montaj talimatlarıdır. Bu, yükleme ve saklama talimatlarını içerir.


15
GCC'yi açtığınızda -O3, en azından biliyorsa pDestve pSrctakma ad vermiyorsa, döngü için SIMD kullanacaktır .
Dietrich Epp

Şu anda 64 bayt (512 bit) SIMD'li bir Xeon Phi üzerinde çalışıyorum, bu yüzden "16 bayta kadar" bu şeyler beni gülümsetiyor. Ek olarak, SIMD'nin etkinleştirilmesi için hedeflediğiniz CPU'yu, örneğin -march = native ile belirtmelisiniz.
yakoudbz

Belki cevabımı gözden geçirmeliyim. :)
onemasse

Bu, gönderme sırasında bile oldukça modası geçmiş. X86 üzerindeki AVX vektörleri (2011'de gönderilir) 32 bayt uzunluğunda ve AVX-512 64 bayt uzunluğundadır. 1024-bit veya 2048-bit vektörlere sahip bazı mimariler ve hatta ARM SVE gibi değişken vektör genişlikleri var
phuclv

@ phuclv talimatlar mevcut olsa da, memcpy'nin bunları kullandığına dair herhangi bir kanıtınız var mı? Normalde kütüphanelerin yetişmesi biraz zaman alır ve bulabildiğim en son olanlar SSSE3'ü kullanıyor ve 2011'den çok daha yeni.
Pete Kirkham

81

Bellek kopyalama rutinleri, aşağıdaki gibi işaretçiler aracılığıyla basit bir bellek kopyalamasından çok daha karmaşık ve daha hızlı olabilir:

void simple_memory_copy(void* dst, void* src, unsigned int bytes)
{
  unsigned char* b_dst = (unsigned char*)dst;
  unsigned char* b_src = (unsigned char*)src;
  for (int i = 0; i < bytes; ++i)
    *b_dst++ = *b_src++;
}

İyileştirmeler

Birinin yapabileceği ilk iyileştirme, işaretçilerden birini bir kelime sınırında hizalamaktır (kelime ile yerel tamsayı boyutunu kastediyorum, genellikle 32 bit / 4 bayt, ancak daha yeni mimarilerde 64 bit / 8 bayt olabilir) ve kelime boyutunda hareket kullanın / copy talimatları. Bu, bir işaretçi hizalanana kadar bir bayt kopyasını bayt olarak kullanmayı gerektirir.

void aligned_memory_copy(void* dst, void* src, unsigned int bytes)
{
  unsigned char* b_dst = (unsigned char*)dst;
  unsigned char* b_src = (unsigned char*)src;

  // Copy bytes to align source pointer
  while ((b_src & 0x3) != 0)
  {
    *b_dst++ = *b_src++;
    bytes--;
  }

  unsigned int* w_dst = (unsigned int*)b_dst;
  unsigned int* w_src = (unsigned int*)b_src;
  while (bytes >= 4)
  {
    *w_dst++ = *w_src++;
    bytes -= 4;
  }

  // Copy trailing bytes
  if (bytes > 0)
  {
    b_dst = (unsigned char*)w_dst;
    b_src = (unsigned char*)w_src;
    while (bytes > 0)
    {
      *b_dst++ = *b_src++;
      bytes--;
    }
  }
}

Kaynağın veya hedef işaretçinin uygun şekilde hizalanıp hizalanmadığına bağlı olarak farklı mimariler farklı şekilde çalışacaktır. Örneğin, bir XScale işlemcide, kaynak işaretçi yerine hedef işaretçiyi hizalayarak daha iyi performans elde ettim.

Performansı daha da artırmak için, işlemcinin yazmaçlarının çoğunun verilerle yüklenebilmesi için bazı döngü açma işlemleri yapılabilir ve bu, yükleme / depolama talimatlarının araya eklenebileceği ve gecikmelerinin ek talimatlarla (döngü sayımı vb.) Gizlenebileceği anlamına gelir. Yükleme / saklama talimatı gecikmeleri oldukça farklı olabileceğinden, bunun getirdiği fayda işlemciye göre oldukça farklılık gösterir.

Bu aşamada kod C (veya C ++) yerine Assembly'de yazılır çünkü gecikme gizleme ve işleme hızından maksimum fayda sağlamak için yükleme ve saklama talimatlarını manuel olarak yerleştirmeniz gerekir.

Genel olarak, verilerin önbellek satırının tamamı, kaydedilmemiş döngünün bir yinelemesinde kopyalanmalıdır.

Bu da beni bir sonraki iyileştirmeye getiriyor, ön getirme ekliyor. Bunlar, işlemcinin önbellek sistemine belleğin belirli bölümlerini önbelleğine yüklemesini söyleyen özel talimatlardır. Talimatın yayınlanması ile önbellek hattının doldurulması arasında bir gecikme olduğu için, talimatların, verinin kopyalanacağı anda ve er ya da geç olmayacak şekilde yerleştirilmesi gerekir.

Bu, ön yükleme talimatlarını işlevin başlangıcına ve ana kopyalama döngüsünün içine koymak anlamına gelir. Kopyalama döngüsünün ortasındaki önceden getirme talimatlarıyla, birkaç yineleme süresinde kopyalanacak verileri alır.

Hatırlayamıyorum, ancak kaynak adreslerin yanı sıra hedef adresleri de önceden getirmek faydalı olabilir.

Faktörler

Belleğin ne kadar hızlı kopyalanabileceğini etkileyen ana faktörler şunlardır:

  • İşlemci, önbellekleri ve ana bellek arasındaki gecikme.
  • İşlemcinin önbellek hatlarının boyutu ve yapısı.
  • İşlemcinin bellek taşıma / kopyalama talimatları (gecikme, verim, kayıt boyutu, vb.).

Dolayısıyla, verimli ve hızlı bir bellek başa çıkma rutini yazmak istiyorsanız, yazdığınız işlemci ve mimari hakkında oldukça fazla bilgiye ihtiyacınız olacak. Gömülü bir platformda yazmıyorsanız, yalnızca yerleşik bellek kopyalama yordamlarını kullanmanın çok daha kolay olacağını söylemek yeterli.


Modern CPU'lar doğrusal bir bellek erişim modelini tespit edecek ve kendi başlarına ön yüklemeye başlayacaktır. Önceden getirme talimatlarının bu nedenle pek bir fark yaratmayacağını umuyorum.
maxy

@maxy Hafıza kopyalama rutinlerini uyguladığım birkaç mimaride önceden getirmeyi eklemek ölçülebilir şekilde yardımcı oldu. Mevcut nesil Intel / AMD yongalarının yeterince ileriye götürüldüğü doğru olsa da, pek çok eski yonga ve diğer mimariler var.
Daemin

kimse "(b_src & 0x3)! = 0" ı açıklayabilir mi? Bunu anlayamıyorum ve ayrıca - derlenmeyecek (bir hata atıyor: geçersiz operatörü ikiliye &: unsigned char ve int);
Maverick Meerkat

"(b_src & 0x3)! = 0", en düşük 2 bitin 0 olup olmadığını kontrol ediyor. Yani, kaynak gösterici 4 baytlık bir katla hizalıysa. Derleme hatanız, 0x3'ü bir in değil bir bayt olarak ele aldığı için olur, bunu 0x00000003 veya 0x3i kullanarak düzeltebilirsiniz (sanırım).
Daemin

b_src & 0x3işaretçi türlerinde bitsel aritmetik yapmanıza izin verilmediğinden derlenmez. Bunu (u)intptr_tilk yapmalısınız
phuclv

18

memcpybilgisayarın mimarisine bağlı olarak aynı anda birden fazla baytı kopyalayabilir. Çoğu modern bilgisayar, tek bir işlemci talimatında 32 bit veya daha fazlasıyla çalışabilir.

Gönderen bir örnek uygulaması :

    00026 * Hızlı kopyalama için, her iki göstericinin de
    00027 * ve uzunluk sözcük hizalıdır ve bunun yerine tek seferde sözcük kopyalayın
    00028 * tek seferde bayt. Aksi takdirde, bayt olarak kopyalayın.

8
Yerleşik önbelleği olmayan bir 386'da (örneğin), bu çok büyük bir fark yarattı. Çoğu modern işlemcide, okuma ve yazma işlemleri her seferinde bir önbellek satırında gerçekleşir ve belleğe giden veri yolu genellikle darboğaz olacaktır, bu nedenle dörde yakın bir yerde değil, yüzde birkaç iyileşme bekleyin.
Jerry Coffin

2
"Kaynaktan" derken biraz daha açık konuşman gerektiğini düşünüyorum. Elbette, bazı mimarilerde "kaynak" budur, ancak kesinlikle, örneğin bir BSD veya Windows makinesinde değildir. (Ve cehennem, GNU sistemleri arasında bile bu işlevde genellikle çok fazla fark vardır)
Billy ONeal

@Billy ONeal: +1 kesinlikle doğru ... Bir kedinin derisini yüzmenin birden fazla yolu var. Bu sadece bir örnekti. Sabit! Yapıcı yorumunuz için teşekkürler.
Mark Byers

7

memcpy()Bazıları performans kazanımları için mimarinize bağlı olan aşağıdaki tekniklerden herhangi birini kullanarak uygulayabilirsiniz ve hepsi kodunuzdan çok daha hızlı olacaktır:

  1. Bayt yerine 32 bit sözcükler gibi daha büyük birimler kullanın. Ayrıca burada uyumla da ilgilenebilirsiniz (veya yapmanız gerekebilir). Örneğin bazı platformlarda garip bir bellek konumuna 32 bitlik bir kelime okuyamaz / yazamazsınız ve diğer platformlarda büyük bir performans cezası ödersiniz. Bunu düzeltmek için, adresin 4'e bölünebilen bir birim olması gerekir. Bunu 64 bit CPU'lar için 64 bit'e kadar veya SIMD (Tek komut, çoklu veri) talimatlarını ( MMX , SSE , vb.) Kullanarak daha da yükseltebilirsiniz .

  2. Derleyicinizin C'den optimize edemeyebileceği özel CPU komutlarını kullanabilirsiniz. Örneğin, bir 80386'da, sayıma N koyarak dikte edilen N baytı taşımak için "rep" önek talimatı + "movsb" komutunu kullanabilirsiniz. Kayıt ol. İyi derleyiciler bunu sadece sizin için yapar, ancak iyi bir derleyiciden yoksun bir platformda olabilirsiniz. Bu örneğin hızın kötü bir gösterimi olma eğiliminde olduğunu, ancak hizalama + daha büyük birim talimatlarıyla birlikte, çoğunlukla belirli CPU'lardaki diğer her şeyden daha hızlı olabileceğini unutmayın.

  3. Döngü açma - bazı CPU'larda dallar oldukça pahalı olabilir, bu nedenle döngüleri açmak dalların sayısını azaltabilir. Bu aynı zamanda SIMD talimatları ve çok büyük boyutlu birimlerle birleştirmek için iyi bir tekniktir.

Örneğin, http://www.agner.org/optimize/#asmlib , memcpyen çok (çok küçük bir miktar) geride kalan bir uygulamaya sahiptir . Kaynak kodunu okursanız, hangi CPU üzerinde çalıştığınıza bağlı olarak bu tekniklerden hangisini seçtiğinizi seçen, yukarıdaki üç tekniğin tümünü kaldıran tonlarca satır içi montaj koduyla dolu olacaktır.

Bir arabellekteki baytları bulmak için de yapılabilecek benzer optimizasyonlar olduğunu unutmayın. strchr()ve arkadaşlar genellikle elinizin devirdiğinden daha hızlı olacaktır. Bu özellikle .NET ve Java için geçerlidir . Örneğin, .NET'te yerleşik , yukarıdaki optimizasyon tekniklerini kullandığı için Boyer – Moore dizgi aramasındanString.IndexOf() bile çok daha hızlıdır .


1
Bağlandığınız aynı Agner Fog, döngü açmanın modern CPU'larda ters etki yarattığını da teorize ediyor .

Günümüzde çoğu CPU'nun iyi dal tahmini vardır ve bu, tipik durumlarda döngü açmanın faydasını ortadan kaldırmalıdır. İyi bir iyileştirme derleyicisi yine de bazen kullanabilir.
thomasrutter

5

Kısa cevap:

  • önbellek doldurma
  • mümkün olan yerlerde bayt olanlar yerine sözcüklü aktarımlar
  • SIMD büyüsü

4

Bunun gerçek dünyadaki herhangi bir uygulamasında gerçekten kullanılıp kullanılmadığını bilmiyorum memcpy, ancak Duff's Device'ın burada bahsetmeyi hak ettiğini düşünüyorum .

Gönderen Vikipedi :

send(to, from, count)
register short *to, *from;
register count;
{
        register n = (count + 7) / 8;
        switch(count % 8) {
        case 0:      do {     *to = *from++;
        case 7:              *to = *from++;
        case 6:              *to = *from++;
        case 5:              *to = *from++;
        case 4:              *to = *from++;
        case 3:              *to = *from++;
        case 2:              *to = *from++;
        case 1:              *to = *from++;
                } while(--n > 0);
        }
}

İşaretçiyi memcpykasıtlı olarak artırmadığı için yukarıdakinin a olmadığını unutmayın to. Biraz farklı bir işlem uygular: bellek eşlemeli bir yazmacıya yazma. Ayrıntılar için Wikipedia makalesine bakın.


Duff'un cihazı veya sadece ilk atlama mekanizması, ilk 1..3 (veya 1..7) baytı kopyalamak için iyi bir kullanımdır, böylece işaretçiler daha büyük bellek hareket talimatlarının kullanılabileceği daha güzel bir sınırla hizalanır.
Daemin

@MarkByers: Kod biraz farklı bir işlemi gösterir ( *tobellek eşlemeli bir kaydı ifade eder ve kasıtlı olarak artırılmaz - bağlantılı makaleye bakın). Netleştirdiğimi düşündüğüm gibi, cevabım verimli bir yöntem sağlamaya çalışmıyor memcpy, sadece oldukça meraklı bir teknikten bahsediyor.
NPE

@Daemin Kabul edildi, dediğin gibi do {} while () 'yı atlayabilirsin ve anahtar derleyici tarafından bir atlama tablosuna çevrilecektir. Kalan verilerle ilgilenmek istediğinizde çok kullanışlıdır. Görünüşe göre daha yeni mimarilerde (daha yeni x86), Duff'un cihazı hakkında bir uyarıdan bahsedilmelidir, dal tahmini o kadar etkilidir ki, Duff'ın cihazı aslında basit bir döngüden daha yavaştır.
onemasse

1
Oh hayır .. Duff'ın cihazı değil. Lütfen Duff'un cihazını kullanmayın. Lütfen. PGO kullanın ve derleyicinin sizin için mantıklı olduğu yerde döngüyü açmasına izin verin.
Billy ONeal 1811

Hayır, Duff'ın cihazı kesinlikle hiçbir modern uygulamada kullanılmıyor.
gnasher729

3

Diğerleri gibi memcpy'nin 1 baytlık parçalardan daha büyük kopyalar olduğunu söylüyor. Kelime boyutundaki yığınları kopyalamak çok daha hızlıdır. Ancak, çoğu uygulama bunu bir adım daha ileri götürür ve döngüye girmeden önce birkaç MOV (word) komutunu çalıştırır. Örneğin döngü başına 8 kelime bloğunu kopyalamanın avantajı, döngünün kendisinin maliyetli olmasıdır. Bu teknik, koşullu dalların sayısını 8 kat azaltarak kopyayı dev bloklar için optimize eder.


1
Bunun doğru olduğunu sanmıyorum. Döngüyü açabilirsiniz, ancak hedef mimaride bir seferde adreslenenden daha fazla veriyi tek bir talimatta kopyalayamazsınız. Ayrıca, döngüyü
açmanın

@Billy ONeal: VoidStar'ın kastettiği şeyin bu olduğunu sanmıyorum. Birkaç ardışık hareket talimatı alarak, birim sayısını saymanın ek yükü azaltılır.
wallyk

@Billy ONeal: Noktayı kaçırıyorsun. Bir seferde 1 kelime MOV, JMP, MOV, JMP, vb. Gibidir. Yapabileceğiniz yer MOV MOV MOV MOV JMP. Daha önce mempcy yazdım ve bunu yapmanın birçok yolunu karşılaştırdım;)
VoidStar

@wallyk: Belki. Ama "daha büyük parçaları kopyala" diyor - ki bu gerçekten mümkün değil. Döngü açmayı kastediyorsa, "çoğu uygulama bunu bir adım daha ileri götürür ve döngüyü açar" demelidir. Yazılan cevap, en iyi ihtimalle yanıltıcı, en kötü ihtimalle yanlıştır.
Billy ONeal

@VoidStar: Kabul edildi --- şimdi daha iyi. +1.
Billy ONeal

2

Cevaplar harika, ancak yine de hızlı bir şekilde memcpykendiniz uygulamak istiyorsanız , hızlı memcpy, C'de hızlı memcpy hakkında ilginç bir blog yazısı var .

void *memcpy(void* dest, const void* src, size_t count)
{
    char* dst8 = (char*)dest;
    char* src8 = (char*)src;

    if (count & 1) {
        dst8[0] = src8[0];
        dst8 += 1;
        src8 += 1;
    }

    count /= 2;
    while (count--) {
        dst8[0] = src8[0];
        dst8[1] = src8[1];

        dst8 += 2;
        src8 += 2;
    }
    return dest;
}

Hatta, bellek erişimlerini optimize etmek daha iyi olabilir.


1

Çünkü birçok kütüphane rutini gibi, üzerinde çalıştığınız mimari için optimize edilmiştir. Diğerleri kullanılabilecek çeşitli teknikler yayınladılar.

Seçime bağlı olarak, kendinizinkini atmak yerine kitaplık rutinlerini kullanın. Bu, DRO (Diğerlerini Tekrar Etme) dediğim DRY'nin bir varyasyonudur. Ayrıca, kütüphane rutinleri kendi uygulamanızdan daha az yanlıştır.

Bellek erişim denetleyicilerinin bellek veya dize arabelleklerinde kelime boyutunun bir katı olmayan sınır dışı okumalardan şikayet ettiğini gördüm. Bu, kullanılan optimizasyonun bir sonucudur.


0

Memset, memcpy ve memmove'un MacOS uygulamasına bakabilirsiniz.

Önyükleme sırasında, işletim sistemi hangi işlemcinin çalıştığını belirler. Desteklenen her işlemci için özel olarak optimize edilmiş kod oluşturmuştur ve önyükleme sırasında bir jmp talimatını, sabit bir salt okunur konumda doğru koda depolar.

C memset, memcpy ve memmove uygulamaları, bu sabit konuma yalnızca bir atlamadır.

Uygulamalar memcpy ve memmove için kaynak ve hedef hizalamasına bağlı olarak farklı kodlar kullanır. Açıkça mevcut tüm vektör yeteneklerini kullanıyorlar. Ayrıca, büyük miktarda veriyi kopyaladığınızda önbelleğe alınmayan varyantları kullanırlar ve sayfa tabloları için bekleme sürelerini en aza indirmek için talimatlara sahiptirler. Bu sadece assembler kodu değil, her işlemci mimarisi hakkında son derece iyi bilgiye sahip biri tarafından yazılmış assembler kodudur.

Intel ayrıca dizi işlemlerini daha hızlı hale getirebilecek montajcı talimatları ekledi. Örneğin, bir döngüde 256 baytı karşılaştıran strstr desteği için bir talimat ile.


Apple'ın açık kaynaklı memset / memcpy / memmove sürümü, SIMD kullanan gerçek sürümden çok daha yavaş olacak genel bir sürüm
phuclv
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.