C int dizisini sıfırlayın: en hızlı yol?


102

Sahip olduğumuzu varsayarsak T myarray[100]T = int, unsigned int, long long int veya unsigned long int ile sahip tüm içeriğini sıfırlamanın en hızlı yolu nedir (sadece başlatma için değil, aynı zamanda içeriği programımda birkaç kez sıfırlamak için) ? Belki memset ile?

Gibi dinamik bir dizi için aynı soru T *myarray = new T[100].


16
@BoPersson: iyi, new bir C ++ ...
Matteo Italia

@Matteo - evet, evet. Cevapları fazla etkilemedi (şimdiye kadar :-).
Bo Persson

3
@BoPersson: Sadece memsetC ++ 'nın bir şekilde işin içine girdiği zaman hakkında konuşurken kendimi kötü hissettim ... :)
Matteo Italia

2
Modern bir derleyicide basit bir fordöngüyü geçemezsiniz . Ama şaşırtıcı bir şekilde, akıllı olmaya çalışarak çok daha kötüsünü yapabilirsiniz.
David Schwartz

Bir yapı kullanın ve içine bir dizi yapıştırın. Tamamen sıfır olan bir örnek oluşturun. Bunu, yarattığınız diğerlerini sıfırlamak için kullanın. İyi çalışıyor. İçerme yok, işlev yok, oldukça hızlı.
Xofo

Yanıtlar:


170

memset(kimden <string.h>) muhtemelen en hızlı standart yoldur, çünkü genellikle doğrudan montajda yazılan ve elle optimize edilen bir rutindir.

memset(myarray, 0, sizeof(myarray)); // for automatically-allocated arrays
memset(myarray, 0, N*sizeof(*myarray)); // for heap-allocated arrays, where N is the number of elements

Bu arada, C ++ 'da deyimsel yol std::fill(from <algorithm>) kullanmak olacaktır :

std::fill(myarray, myarray+N, 0);

ki edilebilir bir halinde otomatik olarak optimize edilebilir memset; İyileştirici yeterince akıllı değilse, daha küçük türler için biraz daha kötü performans gösterirken, s memsetiçin olduğu kadar hızlı çalışacağından oldukça eminim int. Yine de, şüphe duyduğunuzda profil.


10
1999 ISO C standardı itibariyle, memsetbir tamsayının 0'a ayarlanacağı garanti edilmiyordu ; Tüm bit sıfırın bir temsili olduğuna dair özel bir ifade yoktu 0. Bir Teknik Düzeltme, 2011 ISO C standardına dahil olan böyle bir garanti ekledi. Tüm-bit-sıfırın ,0 mevcut tüm C ve C ++ uygulamalarındaki tüm tamsayı türleri için geçerli bir temsili olduğuna inanıyorum , bu nedenle komite bu gereksinimi ekleyebildi. (Kayan nokta veya işaretçi türleri için benzer bir garanti yoktur.)
Keith Thompson

3
@ KeithThompson'ın yorumuna ek olarak: bu garanti, TC2'de (2004) 6.2.6.2 / 5'e düz metin olarak eklendi; ancak hiçbir dolgu biti yoksa, 6.2.6.2/1 ve / 2 zaten tüm bitlerin sıfır olduğunu garanti eder 0. (Dolgu bitleri ile, tüm bit sıfırların bir tuzak temsili olabileceği olasılığı mevcuttur). Ancak her durumda, TC'nin kusurlu metni kabul etmesi ve değiştirmesi beklenir, bu nedenle 2004 itibariyle C99 her zaman bu metni içeriyormuş gibi davranmalıyız.
MM

C'de, dinamik diziyi doğru bir şekilde tahsis ettiyseniz, iki memset arasında fark olmayacaktır. Doğru dinamik ayırma olacaktır int (*myarray)[N] = malloc(sizeof(*myarray));.
Lundin

@Lundin: elbette - derleme zamanında ne kadar büyük olduğunu biliyorsanız N, ancak çoğu durumda kullandıysanız mallocyalnızca çalışma zamanında biliyordunuz.
Matteo Italia

@MatteoItalia 1999 yılından beri VLA'larımız var.
Lundin

20

Bu soru, oldukça eski olmasına rağmen, en deyimsel yolu ya da en az sayıda satırla yazılabilecek yolu değil, en hızlı yolu istediği için bazı ölçütlere ihtiyaç duyar . Ve bu soruyu gerçek bir test yapmadan cevaplamak aptalca. Bu yüzden, dört çözümü karşılaştırdım, memset ile std :: fill ve SIFIR'ın AnT cevabı ile AVX intrinsics kullanarak yaptığım bir çözüm.

Bu çözümün genel olmadığını, yalnızca 32 veya 64 bitlik veriler üzerinde çalıştığını unutmayın. Bu kod yanlış bir şey yapıyorsa lütfen yorum yapın.

#include<immintrin.h>
#define intrin_ZERO(a,n){\
size_t x = 0;\
const size_t inc = 32 / sizeof(*(a));/*size of 256 bit register over size of variable*/\
for (;x < n-inc;x+=inc)\
    _mm256_storeu_ps((float *)((a)+x),_mm256_setzero_ps());\
if(4 == sizeof(*(a))){\
    switch(n-x){\
    case 3:\
        (a)[x] = 0;x++;\
    case 2:\
        _mm_storeu_ps((float *)((a)+x),_mm_setzero_ps());break;\
    case 1:\
        (a)[x] = 0;\
        break;\
    case 0:\
        break;\
    };\
}\
else if(8 == sizeof(*(a))){\
switch(n-x){\
    case 7:\
        (a)[x] = 0;x++;\
    case 6:\
        (a)[x] = 0;x++;\
    case 5:\
        (a)[x] = 0;x++;\
    case 4:\
        _mm_storeu_ps((float *)((a)+x),_mm_setzero_ps());break;\
    case 3:\
        (a)[x] = 0;x++;\
    case 2:\
        ((long long *)(a))[x] = 0;break;\
    case 1:\
        (a)[x] = 0;\
        break;\
    case 0:\
        break;\
};\
}\
}

Düşük seviye optimizasyon uzmanı olmadığım için bunun en hızlı yöntem olduğunu iddia etmeyeceğim. Aksine, memset'ten daha hızlı olan doğru bir mimariye bağlı uygulama örneğidir.

Şimdi sonuçlara bakalım. Boyut 100 int ve uzun uzun diziler için performansı hem statik hem de dinamik olarak tahsis edilmiş olarak hesapladım, ancak statik dizilerde ölü kodu ortadan kaldıran msvc dışında, sonuçlar son derece karşılaştırılabilirdi, bu nedenle yalnızca dinamik dizi performansını göstereceğim. Zaman işaretleri, time.h'nin düşük hassasiyetli saat işlevi kullanılarak 1 milyon yineleme için ms'dir.

clang 3.8 (clang-cl ön ucunu kullanarak, optimizasyon bayrakları = / OX / arch: AVX / Oi / Ot)

int:
memset:      99
fill:        97
ZERO:        98
intrin_ZERO: 90

long long:
memset:      285
fill:        286
ZERO:        285
intrin_ZERO: 188

gcc 5.1.0 (optimizasyon işaretleri: -O3 -march = native -mtune = native -mavx):

int:
memset:      268
fill:        268
ZERO:        268
intrin_ZERO: 91
long long:
memset:      402
fill:        399
ZERO:        400
intrin_ZERO: 185

msvc 2015 (optimizasyon işaretleri: / OX / arch: AVX / Oi / Ot):

int
memset:      196
fill:        613
ZERO:        221
intrin_ZERO: 95
long long:
memset:      273
fill:        559
ZERO:        376
intrin_ZERO: 188

Burada çok ilginç şeyler oluyor: llvm, MSVC'nin tipik sivilceli optimizasyonları olan gcc'yi öldürüyor (statik dizilerde etkileyici bir ölü kod ortadan kaldırıyor ve ardından doldurma için korkunç bir performansa sahip). Uygulamam önemli ölçüde daha hızlı olsa da, bunun nedeni bit temizlemenin diğer ayar işlemlerinden çok daha az ek yüke sahip olduğunu fark etmesi olabilir.

Clang'ın uygulaması, önemli ölçüde daha hızlı olduğu için daha fazla bakmayı hak ediyor. Bazı ek testler, memset'in aslında 400 baytlık dizi için sıfır olmayan - sıfır olmayan memset'ler için özelleştirilmiş olduğunu, çok daha yavaş (~ 220ms) ve gcc'lerle karşılaştırılabilir olduğunu gösteriyor. Bununla birlikte, 800 baytlık bir diziyle sıfırdan farklı memsetting hız farkı yaratmaz, bu nedenle muhtemelen bu durumda, onların memset'leri benim uygulamamdan daha kötü performansa sahiptir - uzmanlık yalnızca küçük diziler içindir ve kesme değeri yaklaşık 800 bayttır. Ayrıca, gcc 'doldurma' ve 'SIFIR'ın memset için optimizasyon yapmadığını (üretilen koda bakarak), gcc'nin aynı performans özelliklerine sahip kod ürettiğini unutmayın.

Sonuç: memset, bu görev için gerçekten optimize edilmemiştir ve insanların öyle olduğunu düşündüğü gibi (aksi takdirde gcc ve msvc ve llvm'nin memset'i aynı performansa sahip olacaktır). Performans önemliyse, memset, özellikle bu garip orta büyüklükteki diziler için nihai bir çözüm olmamalıdır, çünkü bit temizleme için özel değildir ve derleyicinin kendi başına yapabileceğinden daha iyi elle optimize edilmemiştir.


4
Kodsuz ve derleyici sürümünden ve kullanılan seçeneklerden bahsetmeyen bir kıyaslama mı? Hmm ...
Marc Glisse

Derleyici sürümlerine zaten sahiptim (sadece biraz gizlenmişlerdi) ve kullanılan uygulanabilir seçenekleri ekledim.
Benjamin

tekli '*' geçersiz tür bağımsız değişkeni ('size_t {aka unsigned int}' var) |
Piotr Wasilewicz

Kendi optimize edilmiş sıfırlama yönteminizi yazacak kadar cömert davranarak - NASIL çalıştığı ve NEDEN daha hızlı olduğu konusunda birkaç kelime ayırabilir misiniz? kod tamamen açıklayıcıdır.
Motti Shneor

1
@MottiShneor Olduğundan daha karmaşık görünüyor. Bir AVX kaydının boyutu 32 bayttır. Böylece abir sicile kaç değer sığacağını hesaplar . Daha sonra, işaretçi aritmetiği ( (float *)((a)+x)) kullanılarak tamamen üzerine yazılması gereken tüm 32 baytlık bloklar üzerinden döngü yapar . İki içsel (ile başlayan _mm256) sadece sıfır başlatılmış 32 baytlık bir kayıt oluşturur ve bunu geçerli göstericide saklar. Bu ilk 3 satırdır. Gerisi sadece son 32 bayt bloğun tamamen üzerine yazılmaması gereken tüm özel durumları ele alır. Vektörizasyon nedeniyle daha hızlıdır. - Umarım bu yardımcı olur.
wychmaster

11

Kimden memset():

memset(myarray, 0, sizeof(myarray));

Derleme zamanında sizeof(myarray)boyutu myarraybiliniyorsa kullanabilirsiniz . Aksi takdirde, mallocveya aracılığıyla elde edilenler gibi dinamik boyutlu bir dizi kullanıyorsanız new, uzunluğu takip etmeniz gerekecektir.


2
sizeof, dizinin boyutu derleme sırasında bilinmese bile çalışacaktır. (elbette, yalnızca dizi olduğunda)
asaelr

2
@asaelr: C ++ ' sizeofda her zaman derleme zamanında değerlendirilir (ve VLA'larla kullanılamaz). C99'da, VLA'lar durumunda bir çalışma zamanı ifadesi olabilir.
Ben Voigt

@BenVoigt Soru hem cve hem de c++. Alex'in cevabına yorum yaptım, "Eğer dizimin boyutu derleme zamanında biliniyorsa sizeof (myarray) kullanabilirsiniz" diyor.
asaelr

2
@asaelr: Ve C ++ 'da tamamen haklı. Yorumunuz C99 veya VLA'lar hakkında hiçbir şey söylemedi, bu yüzden açıklığa kavuşturmak istedim.
Ben Voigt

5

Kullanabilirsiniz memset, ancak yalnızca tür seçimimiz integral türleriyle sınırlı olduğu için.

Genel olarak C'de bir makro uygulamak mantıklıdır

#define ZERO_ANY(T, a, n) do{\
   T *a_ = (a);\
   size_t n_ = (n);\
   for (; n_ > 0; --n_, ++a_)\
     *a_ = (T) { 0 };\
} while (0)

Bu size C ++ benzeri bir işlevsellik sunarak, herhangi bir türden nesneyi, gibi hack'lere başvurmak zorunda kalmadan "sıfırlara sıfırlamanıza" izin verecektir memset. Temel olarak, bu, tür bağımsız değişkenini açıkça belirtmeniz gerekmesi dışında, C ++ işlev şablonunun bir C analogudur.

Üstelik bozunmayan diziler için bir "şablon" oluşturabilirsiniz.

#define ARRAY_SIZE(a) (sizeof (a) / sizeof *(a))
#define ZERO_ANY_A(T, a) ZERO_ANY(T, (a), ARRAY_SIZE(a))

Örneğinizde şu şekilde uygulanacaktır

int a[100];

ZERO_ANY(int, a, 100);
// or
ZERO_ANY_A(int, a);

Ayrıca özellikle skaler türdeki nesneler için türden bağımsız bir makro uygulanabileceğini belirtmek gerekir.

#define ZERO(a, n) do{\
   size_t i_ = 0, n_ = (n);\
   for (; i_ < n_; ++i_)\
     (a)[i_] = 0;\
} while (0)

ve

#define ZERO_A(a) ZERO((a), ARRAY_SIZE(a))

yukarıdaki örneği

 int a[100];

 ZERO(a, 100);
 // or
 ZERO_A(a);

1
Ben ihmal ediyorum ;sonra while(0)bir çağrı, böylece ZERO(a,n);, 1 büyük bir cevabı
0x90

@ 0x90: Evet, kesinlikle haklısınız. do{}while(0)Deyimin tüm noktası ;makro tanımında hayır gerektiriyor . Sabit.
AnT

3

Statik bildirim için şunu kullanabileceğinizi düşünüyorum:

T myarray[100] = {0};

Dinamik bildirim için aynı yolu öneriyorum: memset


2
Soru şöyle diyor: "Yalnızca başlatma için değil".
Ben Voigt

2

zero(myarray); ihtiyacınız olan tek şey C ++ 'da.

Bunu bir başlığa eklemeniz yeterli:

template<typename T, size_t SIZE> inline void zero(T(&arr)[SIZE]){
    memset(arr, 0, SIZE*sizeof(T));
}

1
Bu yanlış, SIZE baytı temizleyecek. 'memset (arr, 0, SIZE * sizeof (T));' doğru olur.
Kornel Kisielewicz

@KornelKisielewicz D'oh! Umarım son 1,5 yılda hiç kimse bu işlevi kopyalayıp yapıştırmamıştır :(
Navin

1
umarım değil, yorum yaptım çünkü google beni buraya getirdi :)
Kornel Kisielewicz

1
Bu işlevin zero, örneğin bağımsız değişken çok boyutlu bir dizi olduğu T=char[10]durumda olduğu gibi, örneğin doğru olduğuna dikkat edin . arrchar arr[5][10]
mandrake

1
Evet, gcc 4.7.3 ile birkaç vakayı test ettim. Bunu, bu yanıt için not etmenin iyi olacağını düşünüyorum, aksi takdirde her bir dizi boyutu sayısı için şablon uzmanlıklarına ihtiyacınız olacaktır. ARRAY_SIZEÇok boyutlu bir dizide kullanıldığında yanlış boyut veren makro gibi diğer yanıtlar da genelleme yapmaz , belki daha iyi bir isim olurdu ARRAY_DIM<n>_SIZE.
mandrake

1

İşte kullandığım işlev:

template<typename T>
static void setValue(T arr[], size_t length, const T& val)
{
    std::fill(arr, arr + length, val);
}

template<typename T, size_t N>
static void setValue(T (&arr)[N], const T& val)
{
    std::fill(arr, arr + N, val);
}

Şöyle diyebilirsiniz:

//fixed arrays
int a[10];
setValue(a, 0);

//dynamic arrays
int *d = new int[length];
setValue(d, length, 0);

Yukarıda memset kullanmaktan daha C ++ 11 yolu var. Ayrıca boyutu belirterek dinamik dizi kullanırsanız derleme zamanı hatası alırsınız.


orijinal soru C ++ üzerinde değil, bu nedenle std :: fill uygun bir cevap olamaz
Motti Shneor
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.