Đặt lại mảng int C về 0: cách nhanh nhất?


102

Giả sử rằng chúng ta có T myarray[100]với T = int, unsigned int, long long int hoặc unsigned long long int, cách nhanh nhất để đặt lại tất cả nội dung của nó về 0 (không chỉ để khởi tạo mà còn đặt lại nội dung nhiều lần trong chương trình của tôi) ? Có thể với memset?

Câu hỏi tương tự cho một mảng động như T *myarray = new T[100].


16
@BoPersson: à, new C ++ ...
Matteo Italia

@Matteo - vâng. Không ảnh hưởng nhiều đến câu trả lời (cho đến bây giờ :-).
Bo Persson

3
@BoPersson: Tôi chỉ cảm thấy tồi tệ khi nói về việc memsetC ++ bằng cách nào đó có liên quan ... :)
Matteo Italia

2
Trên một trình biên dịch hiện đại, bạn không thể đánh bại một forvòng lặp đơn giản . Tuy nhiên, đáng ngạc nhiên là bạn có thể làm tệ hơn rất nhiều bằng cách cố gắng trở nên thông minh.
David Schwartz

Sử dụng một cấu trúc và gắn một mảng bên trong nó. Tạo một thể hiện có tất cả các số không. Sử dụng điều đó để loại bỏ những người khác mà bạn tạo. Nó hoạt động tốt. Không bao gồm, không có chức năng, khá nhanh.
Xofo

Câu trả lời:


170

memset(from <string.h>) có lẽ là cách chuẩn nhanh nhất, vì nó thường là một quy trình được viết trực tiếp trong lắp ráp và được tối ưu hóa bằng tay.

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

Nhân tiện, trong C ++, cách thành ngữ sẽ là sử dụng std::fill(from <algorithm>):

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

có thể được tối ưu hóa tự động thành một memset; Tôi khá chắc chắn rằng nó sẽ hoạt động nhanh như memsetđối với ints, trong khi nó có thể hoạt động kém hơn một chút đối với các loại nhỏ hơn nếu trình tối ưu hóa không đủ thông minh. Tuy nhiên, khi nghi ngờ, hồ sơ.


10
Theo tiêu chuẩn ISO C 1999, nó thực sự không được đảm bảo rằng memsetsẽ đặt một số nguyên thành 0; không có tuyên bố cụ thể rằng tất cả các bit-0 là đại diện của 0. Một Corrigendum kỹ thuật đã bổ sung một đảm bảo như vậy, được bao gồm trong tiêu chuẩn ISO C 2011. Tôi tin rằng all-bits-zero một đại diện hợp lệ 0cho tất cả các kiểu số nguyên trong tất cả các triển khai C và C ++ hiện có, đó là lý do tại sao ủy ban có thể thêm yêu cầu đó. (Không có đảm bảo tương tự cho dấu chấm động hoặc con trỏ loại.)
Keith Thompson

3
Thêm vào bình luận của @ KeithThompson: đảm bảo này đã được thêm vào 6.2.6.2/5 ở dạng văn bản thuần túy trong TC2 (2004); tuy nhiên nếu không có bit đệm thì 6.2.6.2/1 và / 2 đã được đảm bảo rằng tất cả các bit-không 0. (Với các bit đệm có khả năng tồn tại rằng tất cả các bit-không có thể là một biểu diễn bẫy). Nhưng trong mọi trường hợp, TC phải thừa nhận và thay thế văn bản bị lỗi, vì vậy kể từ năm 2004, chúng tôi nên hành động như thể C99 luôn chứa văn bản này.
MM

Trong C, nếu bạn phân bổ mảng động một cách chính xác , thì sẽ không có sự khác biệt giữa hai memsets. Phân bổ động chính xác sẽ là int (*myarray)[N] = malloc(sizeof(*myarray));.
Lundin

@Lundin: tất nhiên - nếu bạn biết tại thời điểm biên dịch lớn như thế nào N, nhưng trong đại đa số trường hợp nếu bạn sử dụng, mallocbạn chỉ biết trong thời gian chạy.
Matteo Italia

@MatteoItalia Chúng tôi đã có VLA từ năm 1999.
Lundin

20

Câu hỏi này, mặc dù khá cũ, nhưng cần một số điểm chuẩn, vì nó yêu cầu không phải là cách thành ngữ nhất, hoặc cách có thể được viết với số dòng ít nhất, nhưng là cách nhanh nhất . Và thật ngớ ngẩn nếu trả lời câu hỏi đó mà không có một số thử nghiệm thực tế. Vì vậy, tôi đã so sánh bốn giải pháp, memset so với std :: fill so với ZERO của câu trả lời của AnT và giải pháp tôi đã thực hiện bằng cách sử dụng nội dung AVX.

Lưu ý rằng giải pháp này không chung chung, nó chỉ hoạt động trên dữ liệu 32 hoặc 64 bit. Vui lòng bình luận nếu mã này làm điều gì đó không chính xác.

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

Tôi sẽ không khẳng định rằng đây là phương pháp nhanh nhất, vì tôi không phải là chuyên gia tối ưu hóa cấp thấp. Thay vào đó, nó là một ví dụ về việc triển khai phụ thuộc vào kiến ​​trúc chính xác nhanh hơn memset.

Bây giờ, vào kết quả. Tôi đã tính toán hiệu suất cho kích thước 100 int và các mảng dài dài, cả phân bổ tĩnh và động, nhưng ngoại trừ msvc, đã thực hiện loại bỏ mã chết trên các mảng tĩnh, kết quả cực kỳ so sánh, vì vậy tôi sẽ chỉ hiển thị hiệu suất mảng động. Dấu thời gian là ms cho 1 triệu lần lặp, sử dụng chức năng đồng hồ có độ chính xác thấp của time.h.

clang 3.8 (Sử dụng giao diện người dùng clang-cl, cờ tối ưu hóa = / 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 (cờ tối ưu hóa: -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 (cờ tối ưu hóa: / 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

Có rất nhiều điều thú vị đang diễn ra ở đây: llvm giết gcc, tối ưu hóa đốm sáng điển hình của MSVC (nó thực hiện loại bỏ mã chết ấn tượng trên các mảng tĩnh và sau đó có hiệu suất khủng khiếp cho việc lấp đầy). Mặc dù việc triển khai của tôi nhanh hơn đáng kể, nhưng điều này có thể chỉ là do nó nhận ra rằng việc xóa bit có chi phí ít hơn nhiều so với bất kỳ hoạt động cài đặt nào khác.

Việc triển khai của Clang đáng được xem xét nhiều hơn, vì nó nhanh hơn đáng kể. Một số thử nghiệm bổ sung cho thấy rằng memset của nó trên thực tế chuyên dùng cho các memset không - khác 0 cho mảng 400 byte chậm hơn nhiều (~ 220ms) và có thể so sánh với gcc. Tuy nhiên, việc thiết lập bộ nhớ nonzero với mảng 800 byte không tạo ra sự khác biệt về tốc độ, đó có thể là lý do tại sao trong trường hợp đó, bộ nhớ của họ có hiệu suất kém hơn so với cách thực hiện của tôi - chuyên môn hóa chỉ dành cho các mảng nhỏ và giới hạn đúng là khoảng 800 byte. Cũng lưu ý rằng gcc 'fill' và 'ZERO' không tối ưu hóa thành memset (xem mã được tạo), gcc chỉ đơn giản là tạo mã có các đặc điểm hiệu suất giống hệt nhau.

Kết luận: memset không thực sự được tối ưu hóa cho tác vụ này cũng như mọi người sẽ giả vờ như vậy (nếu không thì memset của gcc và msvc và llvm sẽ có cùng hiệu suất). Nếu vấn đề về hiệu suất thì memset không nên là giải pháp cuối cùng, đặc biệt là đối với các mảng có kích thước trung bình khó xử này, bởi vì nó không chuyên dụng để xóa bit và nó không được tối ưu hóa bằng tay tốt hơn so với việc trình biên dịch có thể tự làm.


4
Một điểm chuẩn không có mã và không đề cập đến phiên bản trình biên dịch và các tùy chọn được sử dụng? Hmm ...
Marc Glisse

Tôi đã có các phiên bản trình biên dịch (chúng chỉ bị ẩn một chút) và chỉ cần thêm các tùy chọn áp dụng được sử dụng.
Benjamin

đối số kiểu không hợp lệ của một ngôi '*' (có 'size_t {aka unsigned int}') |
Piotr Wasilewicz

Rất hào phóng để viết phương pháp zeroing được tối ưu hóa của riêng bạn - bạn có thể vui lòng dành vài lời về CÁCH nó hoạt động, và TẠI SAO nó nhanh hơn? mã là tất cả nhưng tự giải thích.
Motti Shneor

1
@MottiShneor Có vẻ phức tạp hơn thực tế. Thanh ghi AVX có kích thước 32byte. Vì vậy, anh ta tính toán có bao nhiêu giá trị của sự aphù hợp trong một thanh ghi. Sau đó, anh ta lặp lại tất cả các khối 32byte, sẽ được ghi đè hoàn toàn bằng cách sử dụng số học con trỏ ( (float *)((a)+x)). Hai bản chất (bắt đầu bằng _mm256) chỉ tạo một thanh ghi 32byte không được khởi tạo và lưu trữ nó vào con trỏ hiện tại. Đây là 3 dòng đầu tiên. Phần còn lại chỉ xử lý tất cả các trường hợp đặc biệt mà khối 32byte cuối cùng không được ghi đè hoàn toàn. Nó nhanh hơn do vector hóa. - Tôi hy vọng điều đó có ích.
wychmaster

11

Từ memset():

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

Bạn có thể sử dụng sizeof(myarray)nếu kích thước của myarrayđược biết trước tại thời điểm biên dịch. Ngược lại, nếu bạn đang sử dụng một mảng có kích thước động, chẳng hạn như thu được qua mallochoặc new, bạn sẽ cần theo dõi độ dài.


2
sizeof sẽ hoạt động ngay cả khi kích thước của mảng không được xác định tại thời điểm biên dịch. (tất nhiên, chỉ khi đó là mảng)
asaelr

2
@asaelr: Trong C ++, sizeofluôn được đánh giá tại thời điểm biên dịch (và không thể sử dụng với VLA). Trong C99, nó có thể là một biểu thức thời gian chạy trong trường hợp VLA.
Ben Voigt

@BenVoigt Chà, câu hỏi là về cả hai cc++. Tôi đã nhận xét về câu trả lời của Alex, điều đó nói rằng "Bạn có thể sử dụng sizeof (myarray) nếu kích thước của myarray được biết tại thời điểm biên dịch".
asaelr

2
@asaelr: Và trong C ++, anh ấy hoàn toàn chính xác. Nhận xét của bạn không nói gì về C99 hoặc VLA, vì vậy tôi muốn làm rõ nó.
Ben Voigt

5

Bạn có thể sử dụng memset, nhưng chỉ vì lựa chọn loại của chúng tôi bị hạn chế đối với loại tích phân.

Trong trường hợp chung trong C, việc triển khai macro

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

Điều này sẽ cung cấp cho bạn chức năng giống như C ++ cho phép bạn "đặt lại về số không" một mảng các đối tượng thuộc bất kỳ loại nào mà không cần phải sử dụng đến các phương pháp hack như memset. Về cơ bản, đây là một dạng tương tự C của mẫu hàm C ++, ngoại trừ việc bạn phải chỉ định đối số kiểu một cách rõ ràng.

Trên hết, bạn có thể tạo một "mẫu" cho các mảng không bị phân rã

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

Trong ví dụ của bạn, nó sẽ được áp dụng là

int a[100];

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

Cũng cần lưu ý rằng đặc biệt đối với các đối tượng thuộc kiểu vô hướng, người ta có thể triển khai macro độc lập với kiểu

#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))

biến ví dụ trên thành

 int a[100];

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

1
Tôi sẽ bỏ qua ;sau khi while(0), vì vậy người ta có thể gọi ZERO(a,n);, 1 câu trả lời lớn
0x90

@ 0x90: Vâng, bạn hoàn toàn đúng. Toàn bộ quan điểm của do{}while(0)thành ngữ không yêu cầu ;trong định nghĩa vĩ mô. Đã sửa.
AnT

3

Đối với khai báo tĩnh, tôi nghĩ bạn có thể sử dụng:

T myarray[100] = {0};

Đối với khai báo động, tôi đề xuất cách tương tự: memset


2
Câu hỏi nói: "Không chỉ để khởi tạo".
Ben Voigt

2

zero(myarray); là tất cả những gì bạn cần trong C ++.

Chỉ cần thêm điều này vào tiêu đề:

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

1
Điều này không chính xác, nó sẽ xóa SIZE byte. 'memset (arr, 0, SIZE * sizeof (T));' sẽ đúng.
Kornel Kisielewicz

@KornelKisielewicz D'oh! Tôi hy vọng không ai sao chép chức năng này trong 1,5 năm qua :(
Navin

1
hy vọng không, tôi nhận xét vì google mang tôi đến đây :)
Kornel Kisielewicz

1
Lưu ý rằng hàm zeronày cũng đúng đối với ví dụ T=char[10]như có thể xảy ra khi arrđối số là một mảng đa chiều, ví dụ char arr[5][10].
mandrake

1
Có, tôi đã thử nghiệm một số trường hợp với gcc 4.7.3. Tôi thấy điều này sẽ rất tốt cần lưu ý cho câu trả lời này, vì nếu không, bạn sẽ cần phải có các chuyên môn về mẫu cho mỗi số kích thước mảng. Các câu trả lời khác cũng không khái quát hóa, chẳng hạn như ARRAY_SIZEmacro, cung cấp kích thước sai nếu được sử dụng trên một mảng đa chiều, có lẽ sẽ là một cái tên tốt hơn ARRAY_DIM<n>_SIZE.
mandrake

1

Đây là chức năng tôi sử dụng:

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

Bạn có thể gọi nó như thế này:

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

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

Trên đây là cách sử dụng C ++ 11 nhiều hơn so với sử dụng memset. Ngoài ra, bạn gặp lỗi thời gian biên dịch nếu bạn sử dụng mảng động với việc chỉ định kích thước.


câu hỏi ban đầu là trên C, không phải C ++, do đó std :: điền có thể không phải là một câu trả lời thích hợp
Motti Shneor
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.