Nếu một số nguyên 32 bit bị tràn, chúng ta có thể sử dụng cấu trúc 40 bit thay vì 64 bit dài không?


76

Giả sử, nếu một số nguyên 32 bit bị tràn, thay vì nâng cấp intlên long, chúng ta có thể sử dụng một số loại 40 bit nào đó không nếu chúng ta chỉ cần một phạm vi trong phạm vi 2 40 , để chúng ta tiết kiệm 24 (64-40) bit cho mỗi số nguyên?

Nếu vậy, làm thế nào?

Tôi phải đối phó với hàng tỷ và không gian là một hạn chế lớn hơn.


5
Ngoài bộ nhớ là rất rẻ so với các chu kỳ CPU để tiết kiệm những byte quý
Ed chữa lành

9
@ user1810087, Aniket ... làm sao bạn biết nó là không cần thiết? Hoặc rằng nó tiêu thụ nhiều byte hơn so với lưu của nó? Bạn có biết các yêu cầu và ràng buộc? Có lẽ anh ta xử lý hàng TB dữ liệu, nơi mà những 'vài byte' đó cộng lại?
Greenflow

24
@Aniket: Tôi thấy một số cơ sở dữ liệu cho điều đó, đặc biệt là khi làm việc với các tập dữ liệu lớn. Tôi hiện đang làm việc với các mô phỏng thể tích trong một khối 1024 ^ 3. Chúng tôi đã triển khai loại dữ liệu 36bit tùy chỉnh, vì điều này tạo ra sự khác biệt cho dù ứng dụng của chúng tôi có thể được sử dụng với RAM 8GB hay không. Ví dụ: 1024 ^ 3 cube với 64bit = 8192MB, 36bit = 4608 bit. Trong trường hợp này, mã nhiều hơn thực sự không quan trọng.
BDL

5
Có một số bộ xử lý thực hiện trong phần cứng số nguyên 40bit (VÍ dụ: một số bộ xử lý Texas Instruments). Nếu bạn đang chạy trên một trong những bộ xử lý này, tôi sẽ nói có, hãy tiếp tục! Nhưng nếu bạn đang sử dụng phần cứng như x86 chỉ có số nguyên 32 hoặc 64 bit, chi phí có thể lớn hơn lợi ích của việc sử dụng số nguyên 40 bit.
Trevor Boyd Smith

24
@All: Còn việc để người dùng1660982 quyết định xem anh ấy / cô ấy có thực sự muốn hay không? Không ai ở đây biết số lượng dữ liệu hoặc tốc độ có quan trọng hay không.
devantfan

Câu trả lời:


82

Đúng nhưng...

Chắc chắn là có thể , nhưng nó thường là vô nghĩa (đối với bất kỳ chương trình nào không sử dụng hàng tỷ con số này):

#include <stdint.h> // don't want to rely on something like long long
struct bad_idea
{
    uint64_t var : 40;
};

Ở đây, varthực sự sẽ có chiều rộng 40 bit với chi phí tạo ra mã kém hiệu quả hơn nhiều (hóa ra là "nhiều" sai rất nhiều - chi phí đo được chỉ là 1-2%, xem thời gian bên dưới), và thường không có kết quả. Trừ khi bạn cần một giá trị 24 bit khác (hoặc một giá trị 8 và 16 bit) mà bạn muốn đóng gói vào cùng một cấu trúc, việc căn chỉnh sẽ mất đi bất cứ thứ gì bạn có thể đạt được.

Trong mọi trường hợp, trừ khi bạn có hàng tỷ trong số này, sự khác biệt hiệu quả về mức tiêu thụ bộ nhớ sẽ không đáng chú ý (nhưng mã bổ sung cần thiết để quản lý trường bit sẽ đáng chú ý!).

Lưu ý:
Trong thời gian trung bình, câu hỏi đã được cập nhật để phản ánh rằng thực sự cần hàng tỷ con số, vì vậy đây có thể là một điều khả thi để làm, giả sử rằng bạn thực hiện các biện pháp để không làm mất lợi ích do liên kết cấu trúc và đệm, tức là bằng cách lưu trữ thứ gì đó khác trong 24 bit còn lại hoặc bằng cách lưu trữ các giá trị 40 bit của bạn trong các cấu trúc của mỗi 8 hoặc bội số của chúng).
Tiết kiệm ba byte một tỷ lần là đáng giá vì nó sẽ yêu cầu ít trang bộ nhớ hơn đáng kể và do đó gây ra ít bộ nhớ cache và bỏ lỡ TLB hơn, và trên tất cả là lỗi trang (lỗi một trang có trọng lượng hàng chục triệu hướng dẫn).

Mặc dù đoạn mã trên không sử dụng 24 bit còn lại (nó chỉ thể hiện phần "sử dụng 40 bit"), nhưng điều gì đó tương tự như sau sẽ là cần thiết để thực sự làm cho cách tiếp cận hữu ích trong ý nghĩa bảo tồn bộ nhớ - giả sử rằng bạn thực sự có dữ liệu "hữu ích" khác để đưa vào các lỗ hổng:

struct using_gaps
{
    uint64_t var           : 40;
    uint64_t useful_uint16 : 16;
    uint64_t char_or_bool  : 8;  
};

Kích thước cấu trúc và sự liên kết sẽ bằng một số nguyên 64 bit, vì vậy sẽ không có gì lãng phí nếu bạn tạo một mảng gồm một tỷ cấu trúc như vậy (ngay cả khi không sử dụng các phần mở rộng dành riêng cho trình biên dịch). Nếu bạn không sử dụng giá trị 8-bit, bạn cũng có thể sử dụng giá trị 48-bit và 16-bit (mang lại lợi nhuận tràn lớn hơn).
Ngoài ra, bạn có thể, với chi phí khả dụng, đặt 8 giá trị 40 bit vào một cấu trúc (bội số chung nhất của 40 và 64 là 320 = 8 * 40). Tất nhiên sau đó mã của bạn mà truy cập các yếu tố trong mảng các cấu trúc sẽ trở thành nhiều phức tạp hơn (mặc dù một lẽ có thể thực hiện một operator[]mà phục hồi các chức năng mảng tuyến tính và da mức độ phức tạp cấu trúc).

Cập nhật:
Đã viết một bộ thử nghiệm nhanh, chỉ để xem các trường bit (và quá tải toán tử với các tham chiếu trường bit) sẽ có gì. Đã đăng mã (do độ dài) tại gcc.godbolt.org , kết quả kiểm tra từ máy Win7-64 của tôi là:

Running test for array size = 1048576
what       alloc   seq(w)  seq(r)  rand(w)  rand(r)  free
-----------------------------------------------------------
uint32_t    0      2       1       35       35       1
uint64_t    0      3       3       35       35       1
bad40_t     0      5       3       35       35       1
packed40_t  0      7       4       48       49       1


Running test for array size = 16777216
what        alloc  seq(w)  seq(r)  rand(w)  rand(r)  free
-----------------------------------------------------------
uint32_t    0      38      14      560      555      8
uint64_t    0      81      22      565      554      17
bad40_t     0      85      25      565      561      16
packed40_t  0      151     75      765      774      16


Running test for array size = 134217728
what        alloc  seq(w)  seq(r)  rand(w)  rand(r)  free
-----------------------------------------------------------
uint32_t    0      312     100     4480     4441     65
uint64_t    0      648     172     4482     4490     130
bad40_t     0      682     193     4573     4492     130
packed40_t  0      1164    552     6181     6176     130

Những gì người ta có thể thấy là chi phí bổ sung của các trường bit là không thể bỏ qua, nhưng việc nạp chồng toán tử với tham chiếu trường bit như một điều tiện lợi là khá mạnh (tăng khoảng 3 lần) khi truy cập dữ liệu tuyến tính theo cách thân thiện với bộ nhớ cache. Mặt khác, khi truy cập ngẫu nhiên, nó hầu như không quan trọng.

Những định thời này cho thấy rằng chỉ cần sử dụng số nguyên 64 bit sẽ tốt hơn vì chúng vẫn nhanh hơn về tổng thể so với trường bit (mặc dù chạm vào nhiều bộ nhớ hơn), nhưng tất nhiên chúng không tính đến chi phí lỗi trang với bộ dữ liệu lớn hơn nhiều. Nó có thể trông rất khác khi bạn dùng hết RAM vật lý (tôi đã không kiểm tra điều đó).


1
Tôi cũng nghĩ như vậy, nhưng các thành viên trường bit có nhiều hơn rằng 32 bit là một phần mở rộng gcc và không phải là một phần của tiêu chuẩn C (hãy thử biên dịch mã của bạn với -Wpedantic).
bitmask

2
Thật thú vị ... clang Groks nó chỉ tốt ở đây (ngay cả với -Wpedantic). GCC của tôi cũng vậy. Ràng buộc đối với 32 bit có thể được nới lỏng với C ++ 11 không?
Damon

2
Trong khi câu trả lời này không sai, nó không thực sự trả lời câu hỏi.
user694733 30/12/14

9
Ngoài ra, các cấu trúc chứa trường bit được đệm vào sự liên kết cấu trúc, dựa trên đơn vị cấp phát trường bit. Vì vậy, nếu điều này hoạt động, cấu trúc sẽ được đệm ra 8 byte và bạn sẽ không tiết kiệm được bất kỳ dung lượng nào.
Chris Dodd

3
Bạn có thể buộc đóng gói byte trong hầu hết các trình biên dịch (đó là một pragma thay đổi từ trình biên dịch này sang trình biên dịch khác), điều này làm cho một mảng của cấu trúc được giảm thiểu phù hợp.
Joshua,

54

Bạn có thể đóng gói các số nguyên 4 * 40bits một cách khá hiệu quả vào một cấu trúc 160 bit như sau:

struct Val4 {
    char hi[4];
    unsigned int low[4];
}

long getLong( const Val4 &pack, int ix ) {
  int hi= pack.hi[ix];   // preserve sign into 32 bit
  return long( (((unsigned long)hi) << 32) + (unsigned long)pack.low[i]);
}

void setLong( Val4 &pack, int ix, long val ) {
  pack.low[ix]= (unsigned)val;
  pack.hi[ix]= (char)(val>>32);
}

Những thứ này lại có thể được sử dụng như thế này:

Val4[SIZE] vals;

long getLong( int ix ) {
  return getLong( vals[ix>>2], ix&0x3 )
}

void setLong( int ix, long val ) {
  setLong( vals[ix>>2], ix&0x3, val )
}

13
Đoạn mã thực sự tiết kiệm bộ nhớ sau khi tính toán phần đệm! +1
Ben Voigt

1
Pro: điều này thực sự tiết kiệm không gian. Con: mã này có lẽ RẤT chậm do lập chỉ mục.
SamB

2
Nó có thể đáng để sử dụng signed char hi[4];một cách rõ ràng; đơn giản charcó thể được ký hoặc không có dấu.
Jonathan Leffler,

4
Nó có thể tốt hơn để sử dụng uint_least32_tint_least8_tở đây, hơn là unsigned intchar. unsigned intchỉ được yêu cầu ít nhất 16 bit. charsẽ luôn có ít nhất 8 bit, vì vậy không có nhiều vấn đề ở đó. Ngoài ra, tôi sẽ sử dụng phép nhân thay vì dịch chuyển bit cho himột phần của giá trị; điều đó được xác định rõ và trình biên dịch có thể thay thế dịch chuyển bit nếu điều đó phù hợp. Ngoài ra, ý kiến ​​hay!
Pete Becker

11
@SamB: Không rõ ràng rằng điều này sẽ "RẤT" chậm. Vấn đề là (giả sử trình biên dịch được thiết lập để tối ưu hóa mạnh mẽ - bao gồm nội tuyến - như nó phải dành cho bất kỳ thứ gì liên quan đến "hàng tỷ" hoạt động!) Tất cả lập chỉ mục tổng hợp thành các hoạt động bên trong CPU trên thanh ghi, điều này có thể được thực hiện trong rất ít chu kỳ (tức là nhanh): thường nhanh hơn nhiều so với việc truy xuất dòng bộ đệm từ bộ nhớ. Vì tổng cộng chúng ta đang truy cập bộ nhớ ít hơn 35% so với trước đây (do tiết kiệm không gian), chúng ta có thể kết thúc với một chiến thắng ròng. (Rõ ràng điều này phụ thuộc vào rất nhiều - đo lường nên :))
psmears

25

Bạn có thể muốn xem xét Mã hóa độ dài biến đổi (VLE)

Có lẽ, bạn đã lưu trữ rất nhiều số đó ở đâu đó (trong RAM, trên đĩa, gửi chúng qua mạng, v.v.), sau đó lấy từng số một và thực hiện một số xử lý.

Một cách tiếp cận sẽ là mã hóa chúng bằng VLE. Từ tài liệu protobuf của Google (giấy phép CreativeCommons)

Biến là một phương pháp tuần tự hóa các số nguyên bằng cách sử dụng một hoặc nhiều byte. Các số nhỏ hơn chiếm số lượng byte nhỏ hơn.

Mỗi byte trong một biến thể, ngoại trừ byte cuối cùng, có bộ bit quan trọng nhất (msb) - điều này cho thấy rằng có nhiều byte tiếp theo. 7 bit thấp hơn của mỗi byte được sử dụng để lưu trữ biểu diễn bổ sung của hai số trong các nhóm 7 bit, nhóm ít có ý nghĩa nhất trước tiên.

Vì vậy, ví dụ, đây là số 1 - nó là một byte duy nhất, vì vậy msb không được đặt:

0000 0001

Và đây là 300 - điều này phức tạp hơn một chút:

1010 1100 0000 0010

Làm thế nào để bạn nhận ra rằng đây là 300? Đầu tiên, bạn thả msb từ mỗi byte, vì điều này chỉ ở đó để cho chúng tôi biết liệu chúng tôi đã đến cuối số hay chưa (như bạn có thể thấy, nó được đặt trong byte đầu tiên vì có nhiều hơn một byte trong biến thể)

Ưu điểm

  • Nếu bạn có nhiều số nhỏ, có thể bạn sẽ sử dụng trung bình ít hơn 40 byte cho mỗi số nguyên. Có thể ít hơn nhiều.
  • Bạn có thể lưu trữ các số lớn hơn (với hơn 40 bit) trong tương lai mà không phải trả tiền phạt cho các số nhỏ

Nhược điểm

  • Bạn trả thêm một bit cho mỗi 7 bit quan trọng trong số của bạn. Điều đó có nghĩa là một số có 40 bit quan trọng sẽ cần 6 byte. Nếu hầu hết các số của bạn có 40 bit quan trọng, bạn nên sử dụng phương pháp tiếp cận trường bit.
  • Bạn sẽ mất khả năng dễ dàng chuyển đến một số được chỉ mục của nó (ít nhất bạn phải phân tích cú pháp một phần tất cả các phần tử trước đó trong một mảng để truy cập vào phần tử hiện tại.
  • Bạn sẽ cần một số hình thức giải mã trước khi làm bất cứ điều gì hữu ích với các con số (mặc dù điều đó cũng đúng với các phương pháp tiếp cận khác, như trường bit)

bạn có thể thay đổi đơn vị nhỏ nhất đến 16 hoặc 32 bit, do đó bạn có thể tiết kiệm rất nhiều bộ nhớ nếu hầu hết các giá trị lớn hơn 1 byte nhưng phù hợp trong vòng 15 hoặc 31 bit
phuclv

3
Nếu các số OP đang cố gắng lưu trữ được phân phối đồng đều, thì sẽ có nhiều số lớn hơn số nhỏ và mã hóa độ dài thay đổi sẽ phản tác dụng.
Russell Borogove

21

(Chỉnh sửa: Trước hết - điều bạn muốn là có thể và có ý nghĩa trong một số trường hợp; tôi đã phải làm những điều tương tự khi cố gắng làm điều gì đó cho thử thách Netflix và chỉ có 1GB bộ nhớ; Thứ hai - có lẽ là tốt nhất sử dụng mảng char cho bộ nhớ 40 bit để tránh bất kỳ vấn đề liên kết nào và sự cần thiết phải xử lý các pragmas đóng gói cấu trúc; Thứ ba - thiết kế này giả định rằng bạn đã chấp nhận số học 64 bit cho các kết quả trung gian, nó chỉ dành cho các lưu trữ mảng mà bạn sẽ sử dụng Int40; Thứ tư: Tôi không nhận được tất cả các đề xuất rằng đây là một ý tưởng tồi, chỉ cần đọc những gì mọi người trải qua để đóng gói cấu trúc dữ liệu lưới và điều này giống như trò chơi của trẻ con khi so sánh).

Những gì bạn muốn là một cấu trúc chỉ được sử dụng để lưu trữ dữ liệu dưới dạng int 40-bit nhưng chuyển đổi ngầm thành int64_t cho số học. Bí quyết duy nhất là làm đúng phần mở rộng dấu hiệu từ 40 lên 64 bit. Nếu bạn ổn với các int không có dấu, mã có thể đơn giản hơn. Điều này sẽ có thể giúp bạn bắt đầu.

#include <cstdint>
#include <iostream>

// Only intended for storage, automatically promotes to 64-bit for evaluation
struct Int40
{
     Int40(int64_t x) { set(static_cast<uint64_t>(x)); } // implicit constructor
     operator int64_t() const { return get(); } // implicit conversion to 64-bit
private:
     void set(uint64_t x)
     {
          setb<0>(x); setb<1>(x); setb<2>(x); setb<3>(x); setb<4>(x);
     };
     int64_t get() const
     {
          return static_cast<int64_t>(getb<0>() | getb<1>() | getb<2>() | getb<3>() | getb<4>() | signx());
     };
     uint64_t signx() const
     {
          return (data[4] >> 7) * (uint64_t(((1 << 25) - 1)) << 39);
     };
     template <int idx> uint64_t getb() const
     {
          return static_cast<uint64_t>(data[idx]) << (8 * idx);
     }
     template <int idx> void setb(uint64_t x)
     {
          data[idx] = (x >> (8 * idx)) & 0xFF;
     }

     unsigned char data[5];
};

int main()
{
     Int40 a = -1;
     Int40 b = -2;
     Int40 c = 1 << 16;
     std::cout << "sizeof(Int40) = " << sizeof(Int40) << std::endl;
     std::cout << a << "+" << b << "=" << (a+b) << std::endl;
     std::cout << c << "*" << c << "=" << (c*c) << std::endl;
}

Đây là liên kết để thử trực tiếp: http://rextester.com/QWKQU25252


Đồng ý với @Andreas, điều này đơn giản với codegen có thể dự đoán được, không giống như các câu trả lời sử dụng trường bit hoặc dựa vào đóng gói dành riêng cho trình biên dịch. Đây là một constexprtriển khai C ++ 17 đã được sửa đổi.
ildjarn

16

Bạn có thể sử dụng cấu trúc trường bit, nhưng nó sẽ không giúp bạn tiết kiệm bất kỳ bộ nhớ nào:

struct my_struct
{
    unsigned long long a : 40;
    unsigned long long b : 24;
};

Bạn có thể ép bất kỳ bội số nào trong số 8 biến 40 bit như vậy vào một cấu trúc:

struct bits_16_16_8
{
    unsigned short x : 16;
    unsigned short y : 16;
    unsigned short z :  8;
};

struct bits_8_16_16
{
    unsigned short x :  8;
    unsigned short y : 16;
    unsigned short z : 16;
};

struct my_struct
{
    struct bits_16_16_8 a1;
    struct bits_8_16_16 a2;
    struct bits_16_16_8 a3;
    struct bits_8_16_16 a4;
    struct bits_16_16_8 a5;
    struct bits_8_16_16 a6;
    struct bits_16_16_8 a7;
    struct bits_8_16_16 a8;
};

Điều này sẽ giúp bạn tiết kiệm một số bộ nhớ (so với việc sử dụng 8 biến 64 bit "tiêu chuẩn"), nhưng bạn sẽ phải chia mọi phép toán (và đặc biệt là các phép toán số học) trên mỗi biến này thành nhiều phép toán.

Vì vậy, việc tối ưu hóa bộ nhớ sẽ được "đánh đổi" để lấy hiệu suất thời gian chạy.


@barakmanos: Bạn có chắc rằng phiên bản mới của mình tốt hơn không?
Ben Voigt

@BenVoigt: Trên VC2013 thì có. Điều tôi không chắc chắn 100% là liệu nó có làm như vậy theo tiêu chuẩn ngôn ngữ hay không, hay nó phụ thuộc vào trình biên dịch. Nếu trường hợp sau là trường hợp, thì a #pragma packnên làm "phần còn lại của công việc". Nhân tiện, có những vấn đề khác ở đây, chẳng hạn như CHAR_BITvề mặt lý thuyết có thể lớn hơn 8, hoặc sizeof(short)về lý thuyết có thể là 1 (ví dụ, nếu CHAR_BITlà 16). Tôi muốn giữ cho câu trả lời đơn giản, dễ đọc hơn là chỉ ra tất cả các trường hợp góc cạnh này.
barak manos

1
@MarcGlisse và theo 64, bạn có nghĩa là 8, vì sizeoftính theo byte.
dùng253751

1
@Inverse: Cảm ơn bạn, nhưng việc bạn chỉnh sửa phần đầu tiên đã khiến lời mở đầu của phần thứ hai trở nên vô nghĩa . Ngoài ra (và thậm chí tệ hơn), nó đã sai - sizeof(my_struct)không phải là 5 byte trên mọi trình biên dịch (hoặc có thể trên bất kỳ trình biên dịch nào). Và trong mọi trường hợp, bạn không thể khởi tạo một mảng các cấu trúc đó sẽ phản ánh 5 byte cho mỗi mục nhập. Vui lòng xác minh các thay đổi của bạn trước khi bạn thực hiện chúng (đặc biệt là trong câu trả lời của những người dùng khác).
barak manos

@immibis Không, ý tôi thực sự là 64, nhưng nhận xét đó đã được đăng trước khi chỉnh sửa (hãy xem lịch sử nếu bạn muốn biết nội dung của nó).
Marc Glisse

9

Như các ý kiến ​​cho thấy, đây là một nhiệm vụ khá.

Có lẽ là một rắc rối không cần thiết trừ khi bạn muốn tiết kiệm rất nhiều RAM - thì điều đó có ý nghĩa hơn nhiều. (Tiết kiệm RAM sẽ là tổng số bit được lưu trên hàng triệu longgiá trị được lưu trữ trong RAM)

Tôi sẽ xem xét sử dụng một mảng 5 byte / ký tự (5 * 8 bit = 40 bit). Sau đó, bạn sẽ cần chuyển các bit từ longgiá trị (int bị tràn - do đó là a ) của bạn vào mảng byte để lưu trữ chúng.

Để sử dụng các giá trị, sau đó chuyển các bit trở lại thành a longvà bạn có thể sử dụng giá trị.

Khi đó, RAM và bộ lưu trữ tệp của bạn có giá trị sẽ là 40 bit (5 byte), NHƯNG bạn phải xem xét việc căn chỉnh dữ liệu nếu bạn định sử dụng a structđể giữ 5 byte. Hãy cho tôi biết nếu bạn cần giải thích kỹ hơn về ý nghĩa của việc dịch chuyển bit và căn chỉnh dữ liệu này.

Tương tự, bạn có thể sử dụng 64 bit longẩn các giá trị khác (có thể là 3 ký tự) trong 24 bit còn lại mà bạn không muốn sử dụng. Một lần nữa - sử dụng dịch chuyển bit để thêm và xóa các giá trị 24 bit.


6

Một biến thể khác có thể hữu ích là sử dụng cấu trúc:

typedef struct TRIPLE_40 {
  uint32_t low[3];
  uint8_t hi[3];
  uint8_t padding;
};

Cấu trúc như vậy sẽ chiếm 16 byte và nếu căn chỉnh 16 byte, sẽ hoàn toàn phù hợp trong một dòng bộ đệm duy nhất. Mặc dù việc xác định phần nào của cấu trúc để sử dụng có thể đắt hơn nếu cấu trúc chứa bốn phần tử thay vì ba phần tử, việc truy cập vào một dòng bộ nhớ cache có thể rẻ hơn nhiều so với truy cập hai. Nếu hiệu suất là quan trọng, người ta nên sử dụng một số điểm chuẩn vì một số máy có thể thực hiện hoạt động divmod-3 với giá rẻ và có chi phí cao cho mỗi lần tìm nạp dòng bộ nhớ cache, trong khi những máy khác có thể có quyền truy cập bộ nhớ rẻ hơn và divmod-3 đắt hơn.


Lưu ý rằng divmod-3 có thể thực sự được thực hiện bằng cách nhân.
SamB

@SamB: Nó thực sự sẽ được thực hiện tốt nhất với một số loại nhân, nhưng điều đó có thể khác nhau giữa các lần triển khai. Trên một cái gì đó như Cortex-M0, một divmod3 của một số 32 bit tùy ý sẽ hơi tốn kém và phải thực hiện các lần tìm nạp hoàn toàn riêng biệt cho các phần 32 bit và các phần 40 bit của một số sẽ không có vấn đề gì.
supercat

6

Tôi sẽ cho rằng

  1. đây là C, và
  2. bạn cần một mảng lớn đơn lẻ gồm các số 40 bit và
  3. bạn đang sử dụng một cỗ máy có thiết kế nhỏ, và
  4. máy của bạn đủ thông minh để xử lý việc căn chỉnh
  5. bạn đã xác định kích thước là số lượng các số 40 bit bạn cần

unsigned char hugearray[5*size+3];  // +3 avoids overfetch of last element

__int64 get_huge(unsigned index)
{
    __int64 t;
    t = *(__int64 *)(&hugearray[index*5]);
    if (t & 0x0000008000000000LL)
        t |= 0xffffff0000000000LL;
    else
        t &= 0x000000ffffffffffLL;
    return t;
}

void set_huge(unsigned index, __int64 value)
{
    unsigned char *p = &hugearray[index*5];
    *(long *)p = value;
    p[4] = (value >> 32);
}

Có thể nhanh hơn để xử lý công việc với hai ca làm việc.

__int64 get_huge(unsigned index)
{
    return (((*(__int64 *)(&hugearray[index*5])) << 24) >> 24);
}

4
Lưu ý rằng mã chứa hành vi không xác định vì unsigned charkhông được đảm bảo có căn chỉnh chính xác cho __int64. Trên một số nền tảng, chẳng hạn như x86-64, nó có thể sẽ không ảnh hưởng nhiều đến bản dựng chưa được tối ưu hóa (mong đợi hiệu suất đạt được) nhưng trên những nền tảng khác, nó có vấn đề - chẳng hạn như ARM. Trên các bản dựng được tối ưu hóa, tất cả các cược đều tắt vì trình biên dịch được phép sử dụng ví dụ như sản xuất mã movaps.
Maciej Piechotka

1
Có lẽ là giải pháp đơn giản nhất của tất cả!
anatolyg,

Chắc chắn, điều này trông hơi xấu trong C với tất cả kiểu ép kiểu, nhưng mã máy tạo ra sẽ đơn giản và nhanh chóng. Phiên bản shift của bạn có khả năng nhanh hơn vì nó không phân nhánh. Nó có thể được tối ưu hóa hơn nữa bằng cách đọc từ 3 byte trước số, do đó tiết kiệm dịch chuyển trái.
aaaaaaaaaaaa

1
Bạn có thể để nó cho trình biên dịch để thực hiện mở rộng dấu hiệu hiệu quả với cách này . Tuy nhiên, điều này nên được kiểm tra cẩn thận vì truy cập không được chỉ định có thể rất tốn kém. Lưu trữ các byte 5th riêng như trong một số giải pháp khác có thể tốt hơn
phuclv

1
Bạn có thể sử dụng memcpyđể thể hiện một cách di động các tải / cửa hàng không được chỉ định, mà không có bất kỳ vi phạm nghiêm ngặt nào về răng cưa như con trỏ truyền đó. Các trình biên dịch hiện đại nhắm mục tiêu x86 (hoặc các nền tảng khác có tải không được đánh dấu hiệu quả) sẽ chỉ sử dụng tải hoặc lưu trữ không được đánh dấu. Ví dụ: đây là ( godbolt.org/g/3BFhWf ) một phiên bản hack của lớp C ++ số nguyên 40 bit của Damon sử dụng a char value[5]và biên dịch thành asm giống như thế này với gcc cho x86-64. (Nếu bạn sử dụng phiên bản đó quá đọc thay vì làm tải riêng biệt, nhưng đó cũng là khá tốt)
Peter Cordes

5

Đối với trường hợp lưu trữ hàng tỷ số nguyên có dấu 40 bit và giả sử là 8 bit có byte, bạn có thể đóng gói 8 số nguyên có dấu 40 bit trong một cấu trúc (trong đoạn mã bên dưới sử dụng một mảng byte để thực hiện điều đó) và, vì cấu trúc này được căn chỉnh thông thường, sau đó bạn có thể tạo một mảng logic gồm các nhóm được đóng gói như vậy và cung cấp lập chỉ mục tuần tự thông thường của cấu trúc đó:

#include <limits.h>     // CHAR_BIT
#include <stdint.h>     // int64_t
#include <stdlib.h>     // div, div_t, ptrdiff_t
#include <vector>       // std::vector

#define STATIC_ASSERT( e ) static_assert( e, #e )

namespace cppx {
    using Byte = unsigned char;
    using Index = ptrdiff_t;
    using Size = Index;

    // For non-negative values:
    auto roundup_div( const int64_t a, const int64_t b )
        -> int64_t
    { return (a + b - 1)/b; }

}  // namespace cppx

namespace int40 {
    using cppx::Byte;
    using cppx::Index;
    using cppx::Size;
    using cppx::roundup_div;
    using std::vector;

    STATIC_ASSERT( CHAR_BIT == 8 );
    STATIC_ASSERT( sizeof( int64_t ) == 8 );

    const int bits_per_value    = 40;
    const int bytes_per_value   = bits_per_value/8;

    struct Packed_values
    {
        enum{ n = sizeof( int64_t ) };
        Byte bytes[n*bytes_per_value];

        auto value( const int i ) const
            -> int64_t
        {
            int64_t result = 0;
            for( int j = bytes_per_value - 1; j >= 0; --j )
            {
                result = (result << 8) | bytes[i*bytes_per_value + j];
            }
            const int64_t first_negative = int64_t( 1 ) << (bits_per_value - 1);
            if( result >= first_negative )
            {
                result = (int64_t( -1 ) << bits_per_value) | result;
            }
            return result;
        }

        void set_value( const int i, int64_t value )
        {
            for( int j = 0; j < bytes_per_value; ++j )
            {
                bytes[i*bytes_per_value + j] = value & 0xFF;
                value >>= 8;
            }
        }
    };

    STATIC_ASSERT( sizeof( Packed_values ) == bytes_per_value*Packed_values::n );

    class Packed_vector
    {
    private:
        Size                    size_;
        vector<Packed_values>   data_;

    public:
        auto size() const -> Size { return size_; }

        auto value( const Index i ) const
            -> int64_t
        {
            const auto where = div( i, Packed_values::n );
            return data_[where.quot].value( where.rem );
        }

        void set_value( const Index i, const int64_t value ) 
        {
            const auto where = div( i, Packed_values::n );
            data_[where.quot].set_value( where.rem, value );
        }

        Packed_vector( const Size size )
            : size_( size )
            , data_( roundup_div( size, Packed_values::n ) )
        {}
    };

}    // namespace int40

#include <iostream>
auto main() -> int
{
    using namespace std;

    cout << "Size of struct is " << sizeof( int40::Packed_values ) << endl;
    int40::Packed_vector values( 25 );
    for( int i = 0; i < values.size(); ++i )
    {
        values.set_value( i, i - 10 );
    }
    for( int i = 0; i < values.size(); ++i )
    {
        cout << values.value( i ) << " ";
    }
    cout << endl;
}

Tôi nghĩ rằng bạn đang giả định phần bổ sung của 2 cho phần mở rộng đăng ký. Nó nghĩ rằng nó phá vỡ với dấu hiệu / độ lớn, nhưng có thể hoạt động với phần bổ sung của 1. Dù sao, đối với phần bổ sung của 2, có lẽ sẽ dễ dàng và hiệu quả hơn khi yêu cầu trình biên dịch ký-mở rộng byte cuối cùng thành 64 bit cho bạn, sau đó HOẶC ở nửa thấp. (Sau đó, trình biên dịch x86 có thể sử dụng movsxtải byte, dịch chuyển và sau đó HOẶC ở 32 bit thấp. Hầu hết các kiến ​​trúc khác cũng có tải hẹp mở rộng dấu hiệu) làm những gì bạn muốn.
Peter Cordes

@PeterCordes: Cảm ơn, có một giả định chưa được đề cập về dạng bổ sung của hai trong đó, vâng. Không biết tại sao tôi lại dựa vào đó. Khó hiểu.
Chúc mừng và hth. - Alf

Tôi sẽ không hy sinh hiệu quả chỉ để làm cho nó di động đến các nền tảng mà không ai sẽ sử dụng nó. Nhưng nếu có thể, hãy sử dụng static_assertđể kiểm tra ngữ nghĩa mà bạn dựa vào.
Peter Cordes

5

Nếu bạn phải xử lý hàng tỷ số nguyên, tôi sẽ cố gắng tổng hợp các mảng gồm các số 40 bit thay vì các số 40 bit đơn lẻ . Bằng cách đó, bạn có thể kiểm tra các triển khai mảng khác nhau (ví dụ: một triển khai nén dữ liệu một cách nhanh chóng hoặc có thể là một triển khai lưu trữ dữ liệu ít được sử dụng vào đĩa.) Mà không cần thay đổi phần còn lại của mã của bạn.

Đây là cách triển khai mẫu ( http://rextester.com/SVITH57679 ):

class Int64Array
{
    char* buffer;
public:
    static const int BYTE_PER_ITEM = 5;

    Int64Array(size_t s)
    {
        buffer=(char*)malloc(s*BYTE_PER_ITEM);
    }
    ~Int64Array()
    {
        free(buffer);
    }

    class Item
    {
        char* dataPtr;
    public:
        Item(char* dataPtr) : dataPtr(dataPtr){}

        inline operator int64_t()
        {
            int64_t value=0;
            memcpy(&value, dataPtr, BYTE_PER_ITEM); // Assumes little endian byte order!
            return value;
        }

        inline Item& operator = (int64_t value)
        {
            memcpy(dataPtr, &value, BYTE_PER_ITEM); // Assumes little endian byte order!
            return *this;
        }
    };   

    inline Item operator[](size_t index) 
    {
        return Item(buffer+index*BYTE_PER_ITEM);
    }
};

Lưu ý: Việc memcpy-conversion từ 40-bit sang 64-bit về cơ bản là hành vi không xác định, vì nó giả định rằng sự kết thúc nhỏ. Tuy nhiên, nó sẽ hoạt động trên nền tảng x86.

Lưu ý 2: Rõ ràng, đây là mã bằng chứng khái niệm, không phải mã sẵn sàng sản xuất. Để sử dụng nó trong các dự án thực tế, bạn phải thêm (trong số những thứ khác):

  • xử lý lỗi (malloc có thể bị lỗi!)
  • sao chép phương thức khởi tạo (ví dụ: bằng cách sao chép dữ liệu, thêm số tham chiếu hoặc bằng cách đặt phương thức khởi tạo sao chép ở chế độ riêng tư)
  • di chuyển hàm tạo
  • const quá tải
  • Trình vòng lặp tương thích với STL
  • kiểm tra giới hạn cho các chỉ số (trong bản dựng gỡ lỗi)
  • phạm vi kiểm tra các giá trị (trong bản dựng gỡ lỗi)
  • khẳng định cho các giả định ngầm định (tính khả thi nhỏ)
  • Vì nó là, Itemcó ngữ nghĩa tham chiếu, không phải ngữ nghĩa giá trị, điều này là bất thường đối với operator[]; Bạn có thể giải quyết vấn đề đó bằng một số thủ thuật chuyển đổi kiểu C ++ thông minh

Tất cả những điều đó sẽ dễ hiểu đối với một lập trình viên C ++, nhưng chúng sẽ làm cho mã mẫu dài hơn nhiều mà không làm rõ ràng hơn, vì vậy tôi đã quyết định bỏ qua chúng.


@anatolyg: Tôi đã cố gắng tóm tắt các điểm của bạn trong Lưu ý 2. Bạn có thể thêm vào danh sách đó ;-)
Niki

3

Có, bạn có thể làm điều đó, và nó sẽ tiết kiệm một số không gian cho số lượng lớn

Bạn cần một lớp chứa một vectơ std :: của một kiểu số nguyên không dấu.

Bạn sẽ cần các hàm thành viên để lưu trữ và lấy một số nguyên. Ví dụ: nếu bạn muốn lưu trữ 64 số nguyên, mỗi số 40 bit, hãy sử dụng một vectơ gồm 40 số nguyên 64 bit mỗi số. Sau đó, bạn cần một phương thức lưu trữ một số nguyên có chỉ mục trong [0,64] và một phương thức để truy xuất một số nguyên như vậy.

Các phương thức này sẽ thực hiện một số hoạt động shift, và cả một số nhị phân | và &.

Tôi không thêm bất kỳ chi tiết nào ở đây vì câu hỏi của bạn không cụ thể lắm. Bạn có biết bạn muốn lưu trữ bao nhiêu số nguyên không? Bạn có biết nó trong thời gian biên dịch? Bạn có biết nó khi chương trình bắt đầu? Các số nguyên nên được tổ chức như thế nào? Giống như một mảng? Giống như một bản đồ? Bạn nên biết tất cả những điều này trước khi cố gắng ép các số nguyên vào bộ nhớ ít hơn.


40 * 64 = 2560bit có thể được giảm xuống lcm (40,64) = 320bit trên mỗi "khối", tức là. 5 64bit-ints
devilantfan

3
std::vector<>chắc chắn không phải là cách để đi: nó có ít nhất ba con trỏ, tức là 96 hoặc 192 bit tùy thuộc vào kiến ​​trúc. Đó là nhiều tồi tệ hơn so với 64 bit của một long long.
cmaster - Khôi phục monica

3
Phụ thuộc. Một std :: vector cho 100000000 số nguyên là tốt. Nếu chúng ta thiết kế các khối nhỏ như trong một câu trả lời khác, std :: vector sẽ rất lãng phí không gian.
Hans Klünder

3

Có khá nhiều câu trả lời ở đây liên quan đến việc triển khai, vì vậy tôi muốn nói về kiến ​​trúc.

Chúng tôi thường mở rộng giá trị 32-bit thành giá trị 64-bit để tránh tràn vì kiến ​​trúc của chúng tôi được thiết kế để xử lý các giá trị 64-bit.

Hầu hết các kiến ​​trúc được thiết kế để làm việc với các số nguyên có kích thước là lũy thừa của 2 vì điều này làm cho phần cứng trở nên đơn giản hơn rất nhiều. Các tác vụ như bộ nhớ đệm đơn giản hơn nhiều theo cách này: có một số lượng lớn các phép toán chia và mô đun có thể được thay thế bằng che bit và dịch chuyển nếu bạn sử dụng quyền hạn của 2.

Như một ví dụ về mức độ quan trọng của điều này, Đặc tả C ++ 11 xác định các trường hợp chủng tộc đa luồng dựa trên "vị trí bộ nhớ". Vị trí bộ nhớ được định nghĩa trong 1.7.3:

Vị trí bộ nhớ là một đối tượng kiểu vô hướng hoặc một chuỗi tối đa các trường bit liền kề, tất cả đều có độ rộng khác không.

Nói cách khác, nếu bạn sử dụng các trường bit của C ++, bạn phải thực hiện tất cả quá trình đa luồng một cách cẩn thận. Hai trường bit liền kề phải được coi là cùng một vị trí bộ nhớ, ngay cả khi bạn muốn các phép tính trên chúng có thể được trải rộng trên nhiều luồng. Điều này rất bất thường đối với C ++, vì vậy có thể gây ra sự thất vọng cho nhà phát triển nếu bạn phải lo lắng về nó.

Hầu hết các bộ vi xử lý đều có cấu trúc bộ nhớ lấy các khối bộ nhớ 32 bit hoặc 64 bit tại một thời điểm. Do đó, việc sử dụng các giá trị 40-bit sẽ có một số lượng truy cập bộ nhớ bổ sung đáng ngạc nhiên, ảnh hưởng đáng kể đến thời gian chạy. Xem xét các vấn đề liên kết:

40-bit word to access:   32-bit accesses   64bit-accesses
word 0: [0,40)           2                 1
word 1: [40,80)          2                 2
word 2: [80,120)         2                 2
word 3: [120,160)        2                 2
word 4: [160,200)        2                 2
word 5: [200,240)        2                 2
word 6: [240,280)        2                 2
word 7: [280,320)        2                 1

Trên kiến ​​trúc 64 bit, một trong số 4 từ sẽ là "tốc độ bình thường". Phần còn lại sẽ yêu cầu tìm nạp gấp đôi dữ liệu. Nếu bạn bị thiếu nhiều bộ nhớ cache, điều này có thể phá hủy hiệu suất. Ngay cả khi bạn nhận được các lần truy cập vào bộ nhớ cache, bạn sẽ phải giải nén dữ liệu và đóng gói lại vào một thanh ghi 64-bit để sử dụng nó (thậm chí có thể liên quan đến một nhánh khó đoán).

Hoàn toàn có thể điều này là xứng đáng với chi phí

Có những tình huống mà những hình phạt này có thể chấp nhận được. Nếu bạn có một lượng lớn dữ liệu thường trú trong bộ nhớ được lập chỉ mục tốt, bạn có thể thấy mức tiết kiệm bộ nhớ đáng bị phạt về hiệu suất. Nếu bạn thực hiện một lượng lớn tính toán trên mỗi giá trị, bạn có thể thấy chi phí là tối thiểu. Nếu vậy, hãy thoải mái thực hiện một trong các giải pháp trên. Tuy nhiên, đây là một vài khuyến nghị.

  • Không sử dụng trường bit trừ khi bạn sẵn sàng trả chi phí của chúng. Ví dụ: nếu bạn có một mảng các trường bit và muốn chia nó ra để xử lý trên nhiều luồng, bạn đang gặp khó khăn. Theo các quy tắc của C ++ 11, tất cả các trường bit đều tạo thành một vị trí bộ nhớ, vì vậy chỉ có thể được truy cập bởi một luồng tại một thời điểm (điều này là do phương thức đóng gói các trường bit được định nghĩa, vì vậy C ++ 11 không thể giúp bạn phân phối chúng theo cách được xác định không triển khai)
  • Không sử dụng cấu trúc chứa số nguyên 32 bit và ký tự tạo thành 40 byte. Hầu hết các bộ xử lý sẽ thực thi căn chỉnh và bạn sẽ không lưu một byte nào.
  • Sử dụng cấu trúc dữ liệu đồng nhất, chẳng hạn như một mảng ký tự hoặc mảng các số nguyên 64 bit. Việc căn chỉnh chính xác sẽ dễ dàng hơn nhiều . (Và bạn cũng giữ quyền kiểm soát việc đóng gói, có nghĩa là bạn có thể chia một mảng giữa một số luồng để tính toán nếu bạn cẩn thận)
  • Thiết kế các giải pháp riêng biệt cho bộ xử lý 32 bit và 64 bit, nếu bạn phải hỗ trợ cả hai nền tảng. Bởi vì bạn đang làm điều gì đó ở mức độ rất thấp và rất ít được hỗ trợ, bạn sẽ cần tùy chỉnh từng thuật toán cho phù hợp với kiến ​​trúc bộ nhớ của nó.
  • Hãy nhớ rằng phép nhân các số 40 bit khác với phép nhân các số mở rộng 64 bit của các số 40 bit được giảm trở lại 40 bit. Cũng giống như khi xử lý FPU x87, bạn phải nhớ rằng việc sắp xếp dữ liệu giữa các kích thước bit sẽ thay đổi kết quả của bạn.

Nếu các số của bạn liền nhau, (ví dụ: struct { char val[5]; };với memcpy), nhiều lần tải hoặc lưu trữ sẽ ở cùng một dòng bộ nhớ cache. Điều đó rẻ (nếu bạn không bị tắc nghẽn về lệnh hoặc thông lượng L1D trước đó) và sẽ không gây ra thêm bộ nhớ cache, nhưng sẽ đánh bại tính năng tự động hóa vectơ, do đó bạn thậm chí có thể không theo kịp bộ nhớ để truy cập tuần tự. (Thông thường, bạn sẽ mong đợi nó biên dịch thành tải 32 bit + 8 bit, trên các mục tiêu hỗ trợ tải không liên kết. X86 hiện đại có hình phạt thấp đối với việc phân chia dòng bộ nhớ cache, mặc dù khi tải từ phân chia trên trang 4k, hình phạt cao hơn).
Peter Cordes

Chiến lược gói / giải nén liên quan đến một nhánh là có thể thực hiện được nhưng hầu như không đáng đề cập trừ khi bạn nhận được thủ công cấp siêu thấp với uintptr_tvà kiểm tra căn chỉnh / tải rộng ( như bạn có thể xem xét trong asm ). Hay bạn đang nói về việc thực hiện điều này trên đầu trang uint64_t []và sử dụng một ifđể tìm hiểu xem bạn chỉ cần một lần tải? Điều đó nghe có vẻ là một ý tưởng tồi so với việc chỉ sử dụng shift để tách / hợp nhất uint64_t thành / từ uint32_tuint8_tvà memcpy hoặc sử dụng cấu trúc để nhóm để căn chỉnh.
Peter Cordes

Theo ISO C ++ 11, bạn có thể phân định "vị trí bộ nhớ" bằng trường bit chiều rộng bằng không. Tôi không chắc tiêu chuẩn ngụ ý rằng một mảng struct __attribute__((packed)) { unsigned long long v:40; };thực sự sẽ là một vị trí bộ nhớ khổng lồ duy nhất; nhưng ngay cả khi ranh giới cấu trúc không phải là ranh giới vị trí bộ nhớ, bạn có thể sử dụng một int end:0để đảm bảo rằng ( lỗi trình biên dịch modulo !, và stackoverflow.com/questions/47008183/… )
Peter Cordes

3

Điều này yêu cầu phát trực tuyến tính năng nén không mất dữ liệu trong bộ nhớ. Nếu đây là ứng dụng Dữ liệu lớn, thì các thủ thuật đóng gói dày đặc là giải pháp chiến thuật tốt nhất cho những gì dường như yêu cầu phần mềm trung gian khá tốt hoặc hỗ trợ cấp hệ thống. Họ cần kiểm tra kỹ lưỡng để đảm bảo người ta có thể khôi phục tất cả các bit mà không hề hấn gì. Và các tác động về hiệu suất rất không tầm thường và phụ thuộc rất nhiều vào phần cứng vì can thiệp vào kiến ​​trúc bộ nhớ đệm của CPU (ví dụ: dòng bộ đệm so với cấu trúc đóng gói). Ai đó đã đề cập đến các cấu trúc chia lưới phức tạp: chúng thường được tinh chỉnh để hợp tác với các kiến ​​trúc bộ nhớ đệm cụ thể.

Không rõ từ các yêu cầu liệu OP có cần truy cập ngẫu nhiên hay không. Với kích thước của dữ liệu, nhiều khả năng người ta sẽ chỉ cần truy cập ngẫu nhiên cục bộ trên các phần tương đối nhỏ, được tổ chức phân cấp để truy xuất. Ngay cả phần cứng cũng thực hiện điều này ở kích thước bộ nhớ lớn (NUMA). Giống như các định dạng phim không mất dữ liệu hiển thị, có thể truy cập ngẫu nhiên theo từng phần ('khung hình') mà không cần phải tải toàn bộ tập dữ liệu vào bộ nhớ nóng (từ kho lưu trữ sao lưu trong bộ nhớ được nén).

Tôi biết một hệ thống cơ sở dữ liệu nhanh (kdb từ KX Systems để đặt tên cho một hệ thống nhưng tôi biết có những hệ thống khác) có thể xử lý các tập dữ liệu cực lớn bằng cách lập bản đồ bộ nhớ các tập dữ liệu lớn từ kho lưu trữ sao lưu. Nó có tùy chọn để nén và mở rộng dữ liệu một cách rõ ràng.


2

Nếu những gì bạn thực sự muốn là một mảng các số nguyên 40 bit (rõ ràng là bạn không thể có), tôi chỉ kết hợp một mảng 32 bit và một mảng số nguyên 8 bit.

Để đọc một giá trị x tại chỉ mục i:

uint64_t x = (((uint64_t) array8 [i]) << 32) + array32 [i];

Để ghi giá trị x vào chỉ mục i:

array8 [i] = x >> 32; array32 [i] = x;

Rõ ràng là được đóng gói độc đáo vào một lớp bằng cách sử dụng các hàm nội tuyến để có tốc độ tối đa.

Có một tình huống mà điều này là không tối ưu, và đó là khi bạn thực sự truy cập ngẫu nhiên vào nhiều mục, do đó mỗi lần truy cập vào một mảng int sẽ là một lần bỏ lỡ bộ nhớ cache - ở đây bạn sẽ nhận được hai lần bỏ lỡ bộ nhớ cache. Để tránh điều này, hãy xác định một cấu trúc 32 byte chứa một mảng sáu uint32_t, một mảng sáu uint8_t và hai byte không sử dụng (41 2/3 bit cho mỗi số); mã để truy cập một mục hơi phức tạp hơn, nhưng cả hai thành phần của mục này đều nằm trong cùng một dòng bộ nhớ cache.


Điều này sẽ không làm những điều khủng khiếp đối với bộ nhớ cache?
SamB
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.