Malloc + Memset neden calloc'dan daha yavaş?


256

Ayrılan belleği başlattığından callocfarklı olduğu bilinmektedir malloc. İle callocbellek sıfıra ayarlanır. İle mallocbellek temizlenmez.

Günlük işlerde + callocolarak görüyorum . Bu arada, eğlence için, bir kıyaslama için aşağıdaki kodu yazdım.mallocmemset

Sonuç kafa karıştırıcı.

Kod 1:

#include<stdio.h>
#include<stdlib.h>
#define BLOCK_SIZE 1024*1024*256
int main()
{
        int i=0;
        char *buf[10];
        while(i<10)
        {
                buf[i] = (char*)calloc(1,BLOCK_SIZE);
                i++;
        }
}

Kod 1 Çıkışı:

time ./a.out  
**real 0m0.287s**  
user 0m0.095s  
sys 0m0.192s  

Kod 2:

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#define BLOCK_SIZE 1024*1024*256
int main()
{
        int i=0;
        char *buf[10];
        while(i<10)
        {
                buf[i] = (char*)malloc(BLOCK_SIZE);
                memset(buf[i],'\0',BLOCK_SIZE);
                i++;
        }
}

Kod 2 Çıkışı:

time ./a.out   
**real 0m2.693s**  
user 0m0.973s  
sys 0m1.721s  

Kod 2 memsetile değiştirmek bzero(buf[i],BLOCK_SIZE)aynı sonucu verir.

Sorum şu:malloc + neden memsetdaha yavaş calloc? Bunu nasıl yapabilirim calloc?

Yanıtlar:


455

Kısa versiyon: Her zaman calloc()yerine kullanın malloc()+memset(). Çoğu durumda, bunlar aynı olacaktır. Bazı durumlarda, tamamen calloc()atlayabileceğinden daha az iş yapar memset(). Diğer durumlarda, calloc()hile yapabilir ve herhangi bir bellek tahsis edemez! Ancak, malloc()+memset()her zaman işin tamamını yapacak.

Bunu anlamak için kısa bir bellek sistemi turu gerekir.

Hızlı bellek turu

Burada dört ana bölüm vardır: programınız, standart kütüphane, çekirdek ve sayfa tabloları. Programınızı zaten biliyorsunuz, yani ...

Gibi Bellek ayırıcılarına malloc()ve calloc()çoğunlukla orada belleğin daha büyük havuzlarına ve grubun onları (KB 100s 1 byte bir şey) küçük tahsisler almak. Örneğin, 16 bayt ayırırsanız malloc(), önce havuzlarından birinden 16 bayt almaya çalışır ve havuz kuru çalıştığında çekirdekten daha fazla bellek ister. Ancak, programın beri yaklaşık seferde büyük bir hafızaya miktarda ayırdığından soruyoruz, malloc()ve calloc()sadece doğrudan çekirdekten bu bellek için soracaktır. Bu davranışın eşiği sisteminize bağlıdır, ancak eşik olarak 1 MiB kullanıldığını gördüm.

Çekirdek, her işleme gerçek RAM ayırmaktan ve işlemlerin diğer işlemlerin belleğine müdahale etmediğinden emin olmaktan sorumludur. Buna bellek koruması denir , 1990'lardan beri kir yaygındır ve bir programın tüm sistemi yıkmadan çökmesinin nedeni budur. Bir program daha fazla belleğe ihtiyaç duyduğunda, sadece belleği almakla kalmaz, bunun yerine mmap()veya gibi bir sistem çağrısı kullanarak çekirdekten bellek ister sbrk(). Çekirdek, sayfa tablosunu değiştirerek her işleme RAM verecektir.

Sayfa tablosu bellek adreslerini gerçek fiziksel RAM ile eşler. 32 bitlik bir sistemde işleminizin adresleri, 0x00000000 - 0xFFFFFFFF, gerçek bellek değil, sanal bellekteki adreslerdir . İşlemci bu adresleri 4 KiB sayfaya böler ve her sayfa, sayfa tablosunu değiştirerek farklı bir fiziksel RAM parçasına atanabilir. Yalnızca çekirdeğin sayfa tablosunu değiştirmesine izin verilir.

Nasıl çalışmıyor

256 MiB gelmez tahsis şekli şöyledir değil çalışır:

  1. calloc()İşleminiz çağırır ve 256 MiB ister.

  2. Standart kütüphane çağırır mmap()ve 256 MiB ister.

  3. Çekirdek 256 MiB kullanılmayan RAM bulur ve sayfa tablosunu değiştirerek işleminize verir.

  4. Standart kütüphane RAM'i sıfırlar memset()ve içinden döner calloc().

  5. İşleminiz sonunda çıkar ve çekirdek RAM'i geri alır, böylece başka bir işlem tarafından kullanılabilir.

Aslında nasıl çalışır

Yukarıdaki süreç işe yarayacaktır, ancak bu böyle olmaz. Üç büyük fark vardır.

  • İşleminiz çekirdekten yeni bellek aldığında, bu bellek muhtemelen daha önce başka bir işlem tarafından kullanılmıştı. Bu bir güvenlik riskidir. Bu bellekte parolalar, şifreleme anahtarları veya gizli salsa tarifleri varsa ne olur? Hassas verilerin sızmasını önlemek için, çekirdek bir işleme başlamadan önce her zaman belleği temizler. Belleği sıfırlayarak da temizleyebiliriz ve eğer yeni bellek sıfırlanırsa, bir garanti de verebiliriz, bu yüzden mmap()döndürdüğü yeni belleğin her zaman sıfırlandığını garanti eder.

  • Dışarıda bellek ayıran ancak belleği hemen kullanmayan birçok program var. Bazı zamanlar bellek ayrılır ancak hiç kullanılmaz. Çekirdek bunu biliyor ve tembel. Yeni bellek ayırdığınızda, çekirdek sayfa tablosuna hiç dokunmaz ve işleminize RAM vermez. Bunun yerine, işleminizde bazı adres alanı bulur, oraya ne gitmesi gerektiğini not eder ve programınız gerçekten kullanıyorsa RAM'i oraya koyacağına dair bir söz verir. Programınız bu adresleri okumaya veya bu adreslerden yazmaya çalıştığında, işlemci bir sayfa hatasını tetikler ve çekirdek bu adreslere RAM atama adımlarını uygular ve programınıza devam eder. Belleği asla kullanmazsanız, sayfa hatası asla olmaz ve programınız RAM'i asla almaz.

  • Bazı işlemler hafızayı tahsis eder ve değiştirmeden hafızadan okur. Bu, farklı işlemlerde bellekteki birçok sayfanın döndürülen bozulmamış sıfırlarla doldurulabileceği anlamına gelir mmap(). Bu sayfaların hepsi aynı olduğundan, çekirdek tüm bu sanal adresleri sıfırlarla dolu tek bir paylaşılan 4 KiB bellek sayfasını işaret eder. Bu belleğe yazmaya çalışırsanız, işlemci başka bir sayfa hatasını tetikler ve çekirdek size başka bir programla paylaşılmayan yeni bir sayfa sıfırlamak için devreye girer.

Son süreç daha çok şöyle görünür:

  1. calloc()İşleminiz çağırır ve 256 MiB ister.

  2. Standart kütüphane çağırır mmap()ve 256 MiB ister.

  3. Çekirdek, 256 MiB kullanılmayan adres alanı bulur , bu adres alanının şu anda ne için kullanıldığını not eder ve geri döner.

  4. Standart kütüphane sonucunun mmap()her zaman sıfırlarla doldurulduğunu bilir (veya gerçekte biraz RAM aldığında olacaktır ), bu yüzden belleğe dokunmaz, bu nedenle sayfa hatası yoktur ve RAM asla işleminize verilmez .

  5. İşleminiz sonunda çıkar ve çekirdeğin RAM'i geri almasına gerek yoktur çünkü asla ilk etapta tahsis edilmemiştir.

memset()Sayfayı sıfırlamak için kullanırsanız memset(), sayfa hatasını tetikler, RAM'in ayrılmasına neden olur ve daha önce sıfırlarla doldurulmuş olsa bile sıfırlar. Bu ekstra iş çok büyük miktarda olduğu ve açıklıyor calloc()daha hızlı olduğunu malloc()ve memset(). Eğer yine de hafızayı kullanmaya son verirseniz calloc(), yine de daha hızlıdır malloc(), memset()ancak fark o kadar saçma değildir.


Bu her zaman işe yaramaz

Tüm sistemlerde disk belleği sanal bellek bulunmadığından, tüm sistemler bu optimizasyonları kullanamaz. Bu, 80286 gibi çok eski işlemcilerin yanı sıra gelişmiş bir bellek yönetim birimi için çok küçük olan tümleşik işlemciler için de geçerlidir.

Bu, her zaman daha küçük ayırmalarla da çalışmayacaktır. Daha küçük ayırmalarla, calloc()doğrudan çekirdeğe gitmek yerine paylaşılan bir havuzdan bellek alır. Genel olarak, paylaşılan havuzda kullanılan ve serbest bırakılan eski bellekten önemsiz veriler depolanabilir, bu free()nedenle calloc()bu belleği alıp memset()temizlemek için çağrılabilir. Ortak uygulamalar, paylaşılan havuzun hangi bölümlerinin bozulmamış ve hala sıfırlarla dolu olduğunu izler, ancak tüm uygulamalar bunu yapmaz.

Bazı yanlış cevapları gönderme

İşletim sistemine bağlı olarak, çekirdek daha sonra sıfırlanmış bellek almanız gerektiğinde boş zamanlarında belleği sıfırlayabilir veya sıfırlamayabilir. Linux vaktinden önce belleği sıfırlamaz ve Dragonfly BSD yakın zamanda bu özelliği çekirdekten de sildi . Diğer bazı çekirdekler ise önceden sıfır bellek kullanır. Boşta dururken sayfaları sıfırlamak, büyük performans farklılıklarını zaten açıklamak için yeterli değildir.

calloc()Fonksiyon bazı özel hafıza hizalı sürümünü kullanmıyor memset()ve bu çok daha hızlı yine de yapmazdım. memset()Modern işlemciler için çoğu uygulama şöyle görünür:

function memset(dest, c, len)
    // one byte at a time, until the dest is aligned...
    while (len > 0 && ((unsigned int)dest & 15))
        *dest++ = c
        len -= 1
    // now write big chunks at a time (processor-specific)...
    // block size might not be 16, it's just pseudocode
    while (len >= 16)
        // some optimized vector code goes here
        // glibc uses SSE2 when available
        dest += 16
        len -= 16
    // the end is not aligned, so one byte at a time
    while (len > 0)
        *dest++ = c
        len -= 1

Gördüğünüz gibi memset(), çok hızlı ve büyük bellek blokları için daha iyi bir şey elde edemeyeceksiniz.

Aslında memset()zaten sıfırlanır bellek sıfırlar hafıza iki kez sıfırlanmasını alır Bu ne anlama geliyor, ama bu sadece bir 2x performans farkı açıklar. Buradaki performans farkı çok daha büyük (sistemimde malloc()+memset()ve arasında üçten fazla büyüklük sırasını ölçtüm calloc()).

Parti hilesi

10 kez döngü yerine, NULL değerine kadar bellek ayıran malloc()veya calloc()NULL döndüren bir program yazın .

Eklerseniz ne olur memset()?


7
@Dietrich: Dietrich'in aynı sıfır dolu sayfayı birçok kez calloc için tahsis ettiği işletim sistemi hakkında sanal bellek açıklamasını kontrol etmek kolaydır. Sadece tahsis edilen her bellek sayfasına önemsiz veri yazan bir döngü ekleyin (her 500 baytta bir bayt yazmak yeterli olacaktır). Sistem her iki durumda da gerçekten farklı sayfalar ayırmaya zorlanacağından, genel sonuç çok daha yakın olmalıdır.
kriss

1
@kriss: gerçekten de, sistemlerin büyük çoğunluğunda her 4096'da bir bayt yeterli olsa da
Dietrich Epp

Aslında, calloc()çoğu zaman bir parçası olan mallocuygulama paketi ve böylece üzere optimize değil diyoruz bzerogelen bellek alırken mmap.
mirabilos

1
Düzenlediğiniz için teşekkürler, neredeyse aklımda olan buydu. Erken malloc + memset yerine her zaman calloc kullanmayı belirtirsiniz. Tamponun küçük bir kısmının sıfırlanması gerekiyorsa, bu 3. parçasını ayarlayın. Aksi halde calloc kullanın. Özellikle tüm boyutu malloc + Memset ETMEYİN (bunun için calloc kullanın) ve valgrind ve statik kod analizörleri (tüm bellek aniden başlatılır) gibi şeyleri engellediğinden her şeyi calloce ETMEYİN. Bunun dışında bence bu iyi.
Ayın çalışanı

5
Hızla ilgili olmasa callocda, daha az hata eğilimli. Başka bir deyişle, large_int * large_inttaşmaya neden olur, calloc(large_int, large_int)döndürülür NULL, ancak malloc(large_int * large_int)döndürülmekte olan bellek bloğunun gerçek boyutunu bilmediğiniz için tanımlanmamış bir davranıştır.
Dunes

12

Çünkü birçok sistemde, yedek işleme süresinde, işletim sistemi boş belleği kendi başına sıfıra ayarlamak ve güvenli olarak işaretlemek için dolaşır calloc(), bu nedenle aradığınızda size calloc()vermek için zaten ücretsiz, sıfırlanmış belleğe sahip olabilir.


2
Emin misiniz? Hangi sistemler bunu yapıyor? Çoğu işletim sisteminin boşta kaldıklarında işlemciyi kapattığını ve o belleğe yazdıkları anda ayrılan işlemler için talep üzerine belleği sıfırladığını düşündüm (ancak ayırdıklarında değil).
Dietrich Epp

@Dietrich - Emin değilim. Bir kez duydum ve calloc()daha verimli hale getirmek için makul (ve oldukça basit) bir yol gibi görünüyordu .
Chris Lutz

@Pierreten - Özel calloc()optimizasyonlar hakkında iyi bir bilgi bulamıyorum ve OP için libc kaynak kodunu yorumlamak istemiyorum. Bu optimizasyonun olmadığını / çalışmadığını göstermek için herhangi bir şey arayabilir misiniz?
Chris Lutz

13
@Dietrich: FreeBSD'nin sayfaları boşta sıfır doldurması beklenir: vm.idlezero_enable ayarına bakın.
Zan Lynx

1
@DietrichEpp necro için özür dileriz, ancak örneğin Windows bunu yapar.
Andreas Grapentin

1

Bazı modlardaki bazı platformlarda, belleği geri göndermeden önce belleği tipik olarak sıfırdan farklı bir değere başlatır, böylece ikinci sürüm belleği iki kez iyi başlatabilir

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.