Den ' pSrc
e N bayt kopyalıyorum pDest
. Bu, tek bir döngüde yapılabilir:
for (int i = 0; i < N; i++)
*pDest++ = *pSrc++
Bu neden memcpy
veya daha yavaş memmove
? Hızlandırmak için hangi numaraları kullanıyorlar?
Den ' pSrc
e N bayt kopyalıyorum pDest
. Bu, tek bir döngüde yapılabilir:
for (int i = 0; i < N; i++)
*pDest++ = *pSrc++
Bu neden memcpy
veya daha yavaş memmove
? Hızlandırmak için hangi numaraları kullanıyorlar?
1
için N
, bu kadar hep gelen 0
etmek N-1
:-)
int
işaretsiz bir tip size_t
kullanılması gerektiğinde sayaç olarak kullanılan daha büyük sorunu görmezden gelir .
memcpy
veya memmove
(işaretçilerin takma ad olup olmadığını anlayıp anlamadıklarına bağlı olarak) dönüştürür.
Yanıtlar:
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.
-O3
, en azından biliyorsa pDest
ve pSrc
takma ad vermiyorsa, döngü için SIMD kullanacaktır .
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:
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.
b_src & 0x3
işaretçi türlerinde bitsel aritmetik yapmanıza izin verilmediğinden derlenmez. Bunu (u)intptr_t
ilk yapmalısınız
memcpy
bilgisayarı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.
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:
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 .
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.
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 , memcpy
en ç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 .
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 memcpy
kası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.
*to
bellek 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.
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.
Cevaplar harika, ancak yine de hızlı bir şekilde memcpy
kendiniz 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.
Çü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.
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.