รีเซ็ต C int array เป็นศูนย์: วิธีที่เร็วที่สุด?


102

สมมติว่าเรามีT myarray[100]T = int, int ที่ไม่ได้ลงนาม, int ยาวหรือ int ยาวที่ไม่ได้ลงนามวิธีใดคือวิธีที่เร็วที่สุดในการรีเซ็ตเนื้อหาทั้งหมดเป็นศูนย์ (ไม่เพียง แต่สำหรับการเริ่มต้น แต่เพื่อรีเซ็ตเนื้อหาหลาย ๆ ครั้งในโปรแกรมของฉัน) เหรอ? อาจจะมี memset?

คำถามเดียวกันสำหรับอาร์เรย์แบบไดนามิกเช่นT *myarray = new T[100].


16
@BoPersson: new ก็คือ C ++ ...
Matteo Italia

@Matteo - อืมใช่ ไม่ส่งผลต่อคำตอบมากนัก (จนถึงตอนนี้ :-)
Bo Persson

3
@BoPersson: ฉันรู้สึกแย่ที่พูดถึงเฉพาะmemsetเมื่อ C ++ เกี่ยวข้องอย่างใด ... :)
Matteo Italia

2
ในคอมไพเลอร์สมัยใหม่คุณไม่สามารถเอาชนะforลูปง่ายๆได้ แต่ที่น่าแปลกใจคือคุณสามารถทำได้แย่กว่านั้นมากโดยพยายามทำตัวให้ฉลาด
David Schwartz

ใช้โครงสร้างและติดอาร์เรย์ไว้ข้างใน สร้างอินสแตนซ์ที่เป็นศูนย์ทั้งหมด ใช้สิ่งนี้เพื่อทำให้คนอื่น ๆ ที่คุณสร้างเป็นศูนย์ มันทำงานได้ดี ไม่รวมไม่มีฟังก์ชั่นค่อนข้างเร็ว
Xofo

คำตอบ:


170

memset(จาก<string.h>) น่าจะเป็นวิธีมาตรฐานที่เร็วที่สุดเนื่องจากโดยปกติจะเป็นกิจวัตรที่เขียนโดยตรงในการประกอบและปรับแต่งด้วยมือ

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

อย่างไรก็ตามใน C ++ วิธีการใช้สำนวนจะใช้std::fill(จาก<algorithm>):

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

ซึ่งอาจปรับให้เหมาะสมโดยอัตโนมัติเป็นmemset; ฉันค่อนข้างแน่ใจว่ามันจะทำงานได้เร็วพอ ๆmemsetกับints ในขณะที่มันอาจทำงานได้แย่กว่าเล็กน้อยสำหรับประเภทที่เล็กกว่าหากเครื่องมือเพิ่มประสิทธิภาพไม่ฉลาดพอ อย่างไรก็ตามเมื่อมีข้อสงสัยโปรไฟล์


10
ตามมาตรฐาน ISO C ปี 1999 ไม่มีการรับประกันว่าmemsetจะตั้งค่าจำนวนเต็มเป็น 0 0ไม่มีคำสั่งเฉพาะที่ทุกบิตเป็นศูนย์เป็นตัวแทนของ Technical Corrigendum ได้เพิ่มการรับประกันดังกล่าวซึ่งรวมอยู่ในมาตรฐาน ISO C ปี 2011 ฉันเชื่อว่า all-bits-zero เป็นตัวแทนที่ถูกต้อง0สำหรับประเภทจำนวนเต็มทั้งหมดในการใช้งาน C และ C ++ ที่มีอยู่ทั้งหมดซึ่งเป็นสาเหตุที่คณะกรรมการสามารถเพิ่มข้อกำหนดดังกล่าวได้ (ไม่มีการรับประกันที่คล้ายกันสำหรับประเภทจุดลอยตัวหรือตัวชี้)
Keith Thompson

3
การเพิ่มความคิดเห็นของ @ KeithThompson: การรับประกันนี้เพิ่มเป็น 6.2.6.2/5 ในข้อความธรรมดาใน TC2 (2004); แต่ถ้าไม่มีบิต padding แล้ว 6.2.6.2/1 และ / 2 0รับประกันแล้วว่าทุกบิตเป็นศูนย์ (ด้วยช่องว่างภายในบิตมีความเป็นไปได้ที่ทุกบิต - ศูนย์อาจเป็นตัวแทนกับดัก) แต่ไม่ว่าในกรณีใด TC ควรจะรับทราบและแทนที่ข้อความที่มีข้อบกพร่องดังนั้นในปี 2547 เราควรทำราวกับว่า C99 มีข้อความนี้เสมอ
MM

ใน C หากคุณจัดสรรอาร์เรย์แบบไดนามิกอย่างถูกต้องจะไม่มีความแตกต่างระหว่างสอง memsets int (*myarray)[N] = malloc(sizeof(*myarray));การจัดสรรแบบไดนามิกที่ถูกต้องจะเป็น
Lundin

@Lundin: แน่นอน - ถ้าคุณรู้ว่าเวลารวบรวมนั้นใหญ่แค่ไหนNแต่ในกรณีส่วนใหญ่ถ้าคุณใช้mallocคุณรู้เฉพาะที่รันไทม์
Matteo Italia

@MatteoItalia เรามี VLAs ตั้งแต่ปี 2542
Lundin

20

คำถามนี้แม้จะค่อนข้างเก่า แต่ก็ต้องการเกณฑ์มาตรฐานบางประการเนื่องจากคำถามนี้ไม่ใช่วิธีที่ใช้สำนวนง่ายที่สุดหรือเป็นวิธีที่สามารถเขียนได้ในจำนวนบรรทัดน้อยที่สุด แต่เป็นวิธีที่เร็วที่สุด และเป็นเรื่องโง่ที่จะตอบคำถามนั้นโดยไม่มีการทดสอบจริง ดังนั้นฉันจึงเปรียบเทียบวิธีแก้ปัญหาสี่วิธี memset กับ std :: fill เทียบกับ ZERO ของคำตอบของ AnT เทียบกับโซลูชันที่ฉันทำโดยใช้ AVX intrinsics

โปรดทราบว่าโซลูชันนี้ไม่ใช่แบบทั่วไป แต่จะใช้ได้กับข้อมูล 32 หรือ 64 บิตเท่านั้น โปรดแสดงความคิดเห็นหากรหัสนี้ทำอะไรไม่ถูกต้อง

#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;\
};\
}\
}

ฉันจะไม่อ้างว่านี่เป็นวิธีที่เร็วที่สุดเนื่องจากฉันไม่ใช่ผู้เชี่ยวชาญด้านการเพิ่มประสิทธิภาพระดับต่ำ แต่เป็นตัวอย่างของการนำไปใช้งานตามสถาปัตยกรรมที่ถูกต้องซึ่งเร็วกว่า memset

ตอนนี้เข้าสู่ผลลัพธ์ ฉันคำนวณประสิทธิภาพสำหรับอาร์เรย์ขนาด 100 int และ long long ทั้งแบบคงที่และแบบไดนามิกที่จัดสรร แต่ยกเว้น msvc ซึ่งกำจัดรหัสตายบนอาร์เรย์แบบคงที่ผลลัพธ์จะเทียบเคียงได้อย่างมากดังนั้นฉันจะแสดงเฉพาะประสิทธิภาพของอาร์เรย์แบบไดนามิก การมาร์กเวลาเป็นมิลลิวินาทีสำหรับการทำซ้ำ 1 ล้านครั้งโดยใช้ฟังก์ชันนาฬิกาที่มีความแม่นยำต่ำของ time.h

เสียงดัง 3.8 (ใช้ฟรอนต์เอนด์ clang, แฟล็กการปรับให้เหมาะสม = / 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 (แฟล็กการเพิ่มประสิทธิภาพ: -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 (แฟล็กการเพิ่มประสิทธิภาพ: / 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

มีหลายสิ่งที่น่าสนใจเกิดขึ้นที่นี่: llvm ฆ่า gcc, การเพิ่มประสิทธิภาพที่ไม่แน่นอนโดยทั่วไปของ MSVC (มันกำจัดรหัสตายที่น่าประทับใจบนอาร์เรย์แบบคงที่แล้วมีประสิทธิภาพที่ยอดเยี่ยมสำหรับการเติม) แม้ว่าการใช้งานของฉันจะเร็วขึ้นอย่างมาก แต่อาจเป็นเพราะตระหนักดีว่าการล้างบิตมีค่าใช้จ่ายน้อยกว่าการดำเนินการตั้งค่าอื่น ๆ

การใช้งานของ Clang นั้นมีประโยชน์มากขึ้นเนื่องจากเร็วกว่าอย่างเห็นได้ชัด การทดสอบเพิ่มเติมบางอย่างแสดงให้เห็นว่า memset มีความเชี่ยวชาญเฉพาะสำหรับ memsets ที่ไม่ใช่ศูนย์สำหรับอาร์เรย์ 400 ไบต์นั้นช้ากว่ามาก (~ 220ms) และเทียบได้กับ gcc อย่างไรก็ตาม memset ที่ไม่ใช่ศูนย์ด้วยอาร์เรย์ 800 ไบต์ไม่ทำให้ความเร็วแตกต่างกันซึ่งอาจเป็นเหตุผลว่าทำไมในกรณีนี้ memset จึงมีประสิทธิภาพที่แย่กว่าการใช้งานของฉันความเชี่ยวชาญเฉพาะสำหรับอาร์เรย์ขนาดเล็กและจุดตัดอยู่ที่ประมาณ 800 ไบต์ โปรดทราบว่า gcc 'fill' และ 'ZERO' ไม่ได้ปรับให้เหมาะกับ memset (ดูจากโค้ดที่สร้างขึ้น) gcc เป็นเพียงการสร้างโค้ดที่มีลักษณะการทำงานที่เหมือนกัน

สรุป: memset ไม่ได้รับการปรับให้เหมาะสมกับงานนี้มากนักเช่นเดียวกับผู้คนจะแสร้งทำเป็นว่าเป็น (มิฉะนั้น memset ของ gcc และ msvc และ llvm จะมีประสิทธิภาพเหมือนกัน) หากประสิทธิภาพมีความสำคัญ memset ไม่ควรเป็นทางออกสุดท้ายโดยเฉพาะอย่างยิ่งสำหรับอาร์เรย์ขนาดกลางที่น่าอึดอัดใจเหล่านี้เนื่องจากไม่มีความเชี่ยวชาญในการล้างบิตและไม่ได้รับการปรับให้เหมาะสมด้วยมือที่ดีกว่าที่คอมไพเลอร์สามารถทำได้ด้วยตัวเอง


4
เกณฑ์มาตรฐานที่ไม่มีรหัสและไม่มีการกล่าวถึงเวอร์ชันคอมไพเลอร์และตัวเลือกที่ใช้? อืม ...
Marc Glisse

ฉันมีเวอร์ชันคอมไพเลอร์อยู่แล้ว (ซ่อนไว้เพียงเล็กน้อย) และเพิ่งเพิ่มตัวเลือกที่เกี่ยวข้องที่ใช้
Benjamin

ประเภทอาร์กิวเมนต์ที่ไม่ถูกต้องของ unary '*' (have 'size_t {aka unsigned int}') |
Piotr Wasilewicz

เป็นคนใจกว้างในการเขียนวิธีการ zeroing ที่ดีที่สุดของคุณเอง - คุณช่วยกรุณาเว้นคำสองสามคำว่ามันทำงานอย่างไรและทำไมจึงเร็วกว่า รหัสเป็นเพียงการอธิบายตัวเอง
Motti Shneor

1
@MottiShneor มันดูซับซ้อนกว่าที่เป็นอยู่ AVX register มีขนาด 32 ไบต์ ดังนั้นเขาจึงคำนวณจำนวนค่าที่aเหมาะสมในทะเบียน หลังจากนั้นเขาวนลูปบนบล็อก 32 ไบต์ทั้งหมดซึ่งควรเขียนทับอย่างสมบูรณ์โดยใช้เลขคณิตตัวชี้ ( (float *)((a)+x)) ทั้งสองภายใน (เริ่มต้นด้วย_mm256) เพียงแค่สร้างรีจิสเตอร์ 32 ไบต์ที่เริ่มต้นเป็นศูนย์และจัดเก็บไว้ในตัวชี้ปัจจุบัน นี่คือ 3 บรรทัดแรก ส่วนที่เหลือจะจัดการกับกรณีพิเศษทั้งหมดที่บล็อก 32 ไบต์สุดท้ายไม่ควรเขียนทับ เร็วขึ้นเนื่องจากการทำให้เป็นเวกเตอร์ - ฉันหวังว่าจะช่วยได้
wychmaster

11

จากmemset():

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

คุณสามารถใช้ได้sizeof(myarray)หากทราบขนาดของmyarrayเวลาคอมไพล์ มิฉะนั้นหากคุณใช้อาร์เรย์ขนาดไดนามิกเช่นได้รับผ่านmallocหรือnewคุณจะต้องติดตามความยาว


2
sizeof จะทำงานแม้ว่าจะไม่ทราบขนาดของอาร์เรย์ในเวลาคอมไพล์ก็ตาม (แน่นอนเฉพาะเมื่อเป็นอาร์เรย์)
asaelr

2
@asaelr: ใน C ++ sizeofจะได้รับการประเมินตามเวลาคอมไพล์เสมอ (และไม่สามารถใช้กับ VLAs ได้) ใน C99 อาจเป็นนิพจน์รันไทม์ในกรณีของ VLAs
Ben Voigt

@BenVoigt ดีคำถามเกี่ยวกับทั้งสองcและc++. ฉันแสดงความคิดเห็นเกี่ยวกับคำตอบของ Alex ซึ่งระบุว่า "คุณสามารถใช้ sizeof (myarray) ได้หากทราบขนาดของ myarray ในเวลาคอมไพล์"
asaelr

2
@asaelr: และใน C ++ เขาถูกต้องสมบูรณ์ ความคิดเห็นของคุณไม่ได้พูดอะไรเกี่ยวกับ C99 หรือ VLAs ดังนั้นฉันจึงต้องการชี้แจง
Ben Voigt

5

คุณสามารถใช้ได้memsetแต่เนื่องจากการเลือกประเภทของเราถูก จำกัด ไว้ที่ประเภทอินทิกรัลเท่านั้น

โดยทั่วไปใน C ควรใช้มาโคร

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

สิ่งนี้จะทำให้คุณมีฟังก์ชันเหมือน C ++ ที่จะช่วยให้คุณ "รีเซ็ตเป็นศูนย์" อาร์เรย์ของวัตถุประเภทใดก็ได้โดยไม่ต้องใช้แฮ็กเช่น memsetเช่น โดยทั่วไปนี่คือ C อะนาล็อกของเทมเพลตฟังก์ชัน C ++ ยกเว้นว่าคุณต้องระบุอาร์กิวเมนต์ชนิดอย่างชัดเจน

ยิ่งไปกว่านั้นคุณสามารถสร้าง "เทมเพลต" สำหรับอาร์เรย์ที่ไม่สลายตัวได้

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

ในตัวอย่างของคุณจะใช้เป็น

int a[100];

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

นอกจากนี้ยังเป็นที่น่าสังเกตว่าโดยเฉพาะสำหรับวัตถุประเภทสเกลาร์เราสามารถใช้มาโครที่ไม่ขึ้นกับชนิดได้

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

และ

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

เปลี่ยนตัวอย่างข้างต้นเป็น

 int a[100];

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

1
ฉันจะละเว้น;หลังจากwhile(0)นั้นเพื่อให้ใครโทรZERO(a,n);ได้ +1 คำตอบที่ดี
0x90

@ 0x90: ใช่คุณพูดถูกจริงๆ จุดรวมของdo{}while(0)สำนวนไม่จำเป็นต้องมี;ในนิยามมาโคร แก้ไขแล้ว.
AnT

3

สำหรับการประกาศแบบคงที่ฉันคิดว่าคุณสามารถใช้:

T myarray[100] = {0};

สำหรับการประกาศแบบไดนามิกฉันขอแนะนำในลักษณะเดียวกัน: memset


2
คำถามระบุว่า: "ไม่เพียง แต่สำหรับการเริ่มต้นเท่านั้น"
Ben Voigt

2

zero(myarray); คือทั้งหมดที่คุณต้องการใน C ++

เพียงเพิ่มสิ่งนี้ลงในส่วนหัว:

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

1
ไม่ถูกต้องจะล้าง SIZE ไบต์ 'memset (arr, 0, SIZE * sizeof (T));' จะถูกต้อง
Kornel Kisielewicz

@KornelKisielewicz D'oh! ฉันหวังว่าจะไม่มีใครคัดลอกวางฟังก์ชันนี้ในช่วง 1.5 ปีที่ผ่านมา :(
Navin

1
หวังว่าจะไม่ฉันแสดงความคิดเห็นเพราะ Google พาฉันมาที่นี่ :)
Kornel Kisielewicz

1
โปรดทราบว่าฟังก์ชั่นนี้zeroยังเป็นที่ถูกต้องสำหรับเช่นT=char[10]เป็นอาจจะเป็นกรณีที่อาร์กิวเมนต์เป็นเช่นอาร์เรย์หลายมิติarr char arr[5][10]
mandrake

1
ใช่ฉันทดสอบหลายกรณีด้วย gcc 4.7.3 ฉันพบว่าสิ่งนี้ควรทราบสำหรับคำตอบนี้เนื่องจากคุณจำเป็นต้องมีความเชี่ยวชาญพิเศษของเทมเพลตสำหรับการนับมิติอาร์เรย์แต่ละรายการ คำตอบอื่น ๆ ไม่ได้คุยเป็นอย่างดีเช่นมาโครซึ่งจะช่วยให้มีขนาดผิดถ้าใช้บนอาร์เรย์หลายมิติชื่อที่ดีกว่าอาจจะเป็นARRAY_SIZE ARRAY_DIM<n>_SIZE
mandrake

1

นี่คือฟังก์ชั่นที่ฉันใช้:

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);
}

เรียกแบบนี้ก็ได้ว่า

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

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

ด้านบนเป็นวิธี C ++ 11 มากกว่าการใช้ memset นอกจากนี้คุณจะได้รับข้อผิดพลาดเกี่ยวกับเวลาในการคอมไพล์หากคุณใช้อาร์เรย์แบบไดนามิกพร้อมระบุขนาด


คำถามเดิมอยู่ที่ C ไม่ใช่ C ++ ดังนั้น std :: fill จึงไม่สามารถเป็นคำตอบที่ถูกต้องได้
Motti Shneor
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.