2008-10-21 43 views
11

Trong mã nguồn mở program I wrote, tôi đang đọc dữ liệu nhị phân (được viết bởi chương trình khác) từ tệp và xuất ints, tăng gấp đôi, và các loại dữ liệu khác. Một trong những thách thức là nó cần phải chạy trên các máy 32 bit và 64 bit của cả hai mức độ cuối cùng, điều này có nghĩa là tôi sẽ phải thực hiện khá nhiều bit ở mức độ thấp. Tôi biết một (rất) một chút về loại bí danh xảo quyệt và nghiêm ngặt và muốn chắc chắn rằng tôi đang làm việc đúng cách.Safe char punning * để tăng gấp đôi trong C

Về cơ bản, thật dễ dàng để chuyển đổi từ một char * đến một int có kích thước khác nhau:

int64_t snativeint64_t(const char *buf) 
{ 
    /* Interpret the first 8 bytes of buf as a 64-bit int */ 
    return *(int64_t *) buf; 
} 

và tôi có một dàn diễn viên của chức năng hỗ trợ để trao đổi lệnh byte khi cần thiết, chẳng hạn như :

int64_t swappedint64_t(const int64_t wrongend) 
{ 
    /* Change the endianness of a 64-bit integer */ 
    return (((wrongend & 0xff00000000000000LL) >> 56) | 
      ((wrongend & 0x00ff000000000000LL) >> 40) | 
      ((wrongend & 0x0000ff0000000000LL) >> 24) | 
      ((wrongend & 0x000000ff00000000LL) >> 8) | 
      ((wrongend & 0x00000000ff000000LL) << 8) | 
      ((wrongend & 0x0000000000ff0000LL) << 24) | 
      ((wrongend & 0x000000000000ff00LL) << 40) | 
      ((wrongend & 0x00000000000000ffLL) << 56)); 
} 

Khi chạy, chương trình sẽ dò tìm endianness của máy và gán một trong những trên để một con trỏ hàm:

int64_t (*slittleint64_t)(const char *); 
if(littleendian) { 
    slittleint64_t = snativeint64_t; 
} else { 
    slittleint64_t = sswappedint64_t; 
} 

Bây giờ, phần phức tạp xảy ra khi tôi cố gắng truyền một từ * thành gấp đôi. Tôi muốn muốn tái sử dụng mã endian-trao đổi như sau:

union 
{ 
    double d; 
    int64_t i; 
} int64todouble; 

int64todouble.i = slittleint64_t(bufoffset); 
printf("%lf", int64todouble.d); 

Tuy nhiên, một số trình biên dịch có thể tối ưu hóa đi những "int64todouble.i" phân và phá vỡ các chương trình. Có cách nào an toàn hơn để thực hiện việc này không, trong khi xem xét rằng chương trình này phải được tối ưu hóa cho hiệu suất và tôi cũng muốn viết một tập song song các phép biến đổi để truyền char * thành gấp đôi trực tiếp? Nếu phương pháp hợp nhất của punning là an toàn, tôi có nên viết lại các chức năng của tôi như snativeint64_t để sử dụng nó không?


tôi đã kết thúc bằng Steve Jessop's câu trả lời vì các chức năng chuyển đổi lại bằng văn bản để sử dụng memcpy, như vậy:

int64_t snativeint64_t(const char *buf) 
{ 
    /* Interpret the first 8 bytes of buf as a 64-bit int */ 
    int64_t output; 
    memcpy(&output, buf, 8); 
    return output; 
} 

biên dịch vào cùng lắp ráp chính xác như mã ban đầu của tôi:

snativeint64_t: 
     movq (%rdi), %rax 
     ret 

Trong số hai, phiên bản memcpy rõ ràng hơn thể hiện những gì tôi đang cố gắng làm và nên làm việc ngay cả những trình biên dịch ngây thơ nhất.

Adam, câu trả lời của bạn cũng tuyệt vời và tôi đã học được rất nhiều từ nó. Cảm ơn vì đăng!

+0

Việc chuyển đổi số nguyên chỉ an toàn nếu con trỏ char được căn chỉnh đủ tốt. –

+0

Trong chương trình này, nó sẽ luôn luôn được. –

+0

tại sao phát hiện endian-ness ở thời gian chạy? Tôi sẽ tưởng tượng chương trình chỉ có thể làm việc trên vòm nó đã được biên dịch mà sẽ có một endian-ness cụ thể (tôi biết một số cho phép bạn chọn), vậy tại sao không làm như ntohl và như thế và làm cho nó một quyết định thời gian biên dịch? –

Trả lời

2

Vì bạn dường như biết đủ về triển khai của mình để chắc chắn rằng int64_t và double có cùng kích thước và có các đại diện lưu trữ phù hợp, bạn có thể gây nguy hiểm cho memcpy. Sau đó, bạn thậm chí không cần phải suy nghĩ về răng cưa.

Vì bạn đang sử dụng con trỏ hàm cho hàm có thể dễ dàng được gạch chân nếu bạn sẵn sàng phát hành nhiều tệp nhị phân, hiệu suất không phải là vấn đề lớn, nhưng bạn có thể muốn biết rằng một số trình biên dịch có thể khá Bạn có thể tìm thấy các biến được tối ưu hóa hoàn toàn và trình biên dịch thực hiện "sao chép" chỉ đơn giản là gán lại các ngăn xếp mà nó sử dụng cho các biến, như một công đoàn.

int64_t i = slittleint64_t(buffoffset); 
double d; 
memcpy(&d,&i,8); /* might emit no code if you're lucky */ 
printf("%lf", d); 

Kiểm tra mã kết quả hoặc chỉ cấu hình mã. Rất có thể là ngay cả trong trường hợp xấu nhất nó sẽ không được làm chậm.

Nói chung, mặc dù, làm bất cứ điều gì quá thông minh với kết quả bytewapping trong các vấn đề về tính di động. Có tồn tại ABIs với đôi trung lưu, nơi mỗi từ nhỏ bé, nhưng từ lớn đến trước.

Thông thường bạn có thể xem xét lưu trữ đôi của bạn bằng cách sử dụng sprintf và sscanf, nhưng đối với dự án của bạn các định dạng tệp không nằm trong tầm kiểm soát của bạn. Nhưng nếu ứng dụng của bạn chỉ đơn giản hóa việc tăng gấp đôi so với tệp đầu ra ở định dạng khác (không chắc chắn là vì tôi không biết định dạng cơ sở dữ liệu được đề cập, nhưng nếu có) thì có lẽ bạn có thể quên đi thực tế rằng nó là một đôi, vì bạn không sử dụng nó cho số học anyway. Chỉ coi nó là char mờ [8], chỉ cần bytewapping nếu các định dạng tệp khác nhau.

+0

Mẹo memcpy tuyệt vời - cảm ơn! Tôi thực sự cần sản lượng tăng gấp đôi ở dạng văn bản hoặc tôi chỉ cần treo các byte thô xung quanh. Ngoài ra, tôi đã lược tả nó rất nhiều và không có con trỏ hàm (vì tôi sẵn sàng bỏ qua phần cuối lớn nếu nó có nhiều hiệu ứng) nhưng không có sự khác biệt có thể đo lường được. –

12

Tôi khuyên bạn nên đọc Understanding Strict Aliasing. Cụ thể, hãy xem các phần có nhãn "Truyền qua công đoàn". Nó có một số ví dụ rất hay. Trong khi bài báo nằm trên một trang web về bộ xử lý Cell và sử dụng các ví dụ lắp ráp PPC, hầu như tất cả đều tương thích với các kiến ​​trúc khác, bao gồm x86.

+0

Cảm ơn! Đó là thứ tôi đang tìm kiếm. Tôi sẽ đọc ngay bây giờ. –

+0

@ryan_s: Cảm ơn, cố định –

2

Tiêu chuẩn nói rằng việc ghi vào một lĩnh vực của một công đoàn và đọc từ đó ngay lập tức là hành vi không xác định. Vì vậy, nếu bạn đi theo cuốn sách quy tắc, phương pháp dựa trên công đoàn sẽ không hoạt động.

Macro thường là ý tưởng tồi, nhưng điều này có thể là ngoại lệ đối với quy tắc.Có thể nhận được hành vi giống như mẫu trong C bằng cách sử dụng một tập các macro bằng cách sử dụng các kiểu đầu vào và đầu ra làm tham số.

+0

Hướng dẫn GCC nói rằng "Ngay cả với bí danh-giới hạn, loại-punning được cho phép, miễn là bộ nhớ được truy cập thông qua loại công đoàn." It's * so * hấp dẫn để gọi đó là đủ tốt, nhưng tôi ghét phải viết mã trình biên dịch cụ thể. Có một con trỏ đến một ví dụ vĩ mô? –

0

Là một đề xuất phụ rất nhỏ, tôi khuyên bạn nên điều tra xem bạn có thể hoán đổi mặt nạ và dịch chuyển, trong trường hợp 64 bit hay không. Kể từ khi hoạt động được trao đổi byte, bạn sẽ có thể luôn luôn nhận được ngay với một mặt nạ chỉ 0xff. Điều này sẽ dẫn đến mã nhanh hơn, nhỏ gọn hơn, trừ khi trình biên dịch đủ thông minh để tìm ra chính nó.

Tóm lại, việc thay đổi này:

(((wrongend & 0xff00000000000000LL) >> 56) 

vào đây:

((wrongend >> 56) & 0xff) 

nên tạo ra kết quả tương tự.

+0

Điều đó sẽ chỉ hoạt động cho hoạt động mặt nạ và dịch chuyển đầu tiên vì tất cả những người khác đang di chuyển bit vào giữa đầu ra. –

+0

True, sau đó bạn phải chuyển nó trở lại sau khi mặt nạ. Tôi có lẽ sẽ thích làm điều đó, vì tránh các hằng số rất lớn (đối với tôi) là tốt đẹp. Chỉ cần hoán đổi thứ tự hoạt động tốt hơn khi bạn trích xuất các byte và làm một cái gì đó khác với sau đó, byte-cho-byte. – unwind

-1

Edit:
comments Removed về làm thế nào để lưu trữ một cách hiệu quả dữ liệu luôn luôn lớn endian và trao đổi để máy endianess, như hỏi đã không được đề cập một chương trình khác ghi dữ liệu của mình (đó là thông tin quan trọng). Tuy nhiên, nếu dữ liệu cần chuyển đổi từ bất kỳ endian nào thành lớn và từ big to host endian, ntohs/ntohl/htons/htonl là phương pháp tốt nhất, thanh lịch nhất và cạnh tranh nhất về tốc độ (vì chúng sẽ thực hiện nhiệm vụ trong phần cứng nếu CPU hỗ trợ , bạn không thể đánh bại điều đó).


Về đôi/phao, chỉ cần lưu trữ chúng để ints bằng phương pháp đúc bộ nhớ:

double d = 3.1234; 
printf("Double %f\n", d); 
int64_t i = *(int64_t *)&d; 
// Now i contains the double value as int 
double d2 = *(double *)&i; 
printf("Double2 %f\n", d2); 

Wrap nó vào một hàm

int64_t doubleToInt64(double d) 
{ 
    return *(int64_t *)&d; 
} 

double int64ToDouble(int64_t i) 
{ 
    return *(double *)&i; 
} 

Người hỏi cung cấp liên kết này:

http://cocoawithlove.com/2008/04/using-pointers-to-recast-in-c-is-bad.html

để chứng minh rằng truyền là xấu ... tiếc là tôi chỉ có thể không đồng ý mạnh mẽ với hầu hết trang này. Báo giá và nhận xét:

Thông thường, việc truyền qua con trỏ là, thực tế là thực tế không tốt và mã có khả năng nguy hiểm. Đúc thông qua một con trỏ có khả năng để tạo lỗi do loại xảo quyệt.

Đó không phải là rủi ro chút nào và đó cũng không phải là hành vi không tốt. Nó chỉ có khả năng gây ra lỗi nếu bạn làm điều đó không chính xác, giống như lập trình trong C có khả năng gây ra lỗi nếu bạn làm điều đó không chính xác, cũng như bất kỳ chương trình nào bằng bất kỳ ngôn ngữ nào. Bởi lập luận đó, bạn phải ngừng lập trình hoàn toàn.

Loại punning
Một dạng của con trỏ răng cưa nơi hai con trỏ và tham khảo đến cùng một vị trí trong bộ nhớ nhưng đại diện cho vị trí đó là khác nhau loại. Trình biên dịch sẽ xử lý cả hai " " chơi chữ "làm con trỏ không liên quan.Loại punning có khả năng gây ra sự cố phụ thuộc cho bất kỳ dữ liệu nào được truy cập thông qua cả hai con trỏ.

Điều này đúng, nhưng không may là hoàn toàn không liên quan đến mã của tôi.

gì ông đề cập đến là mã như thế này:

int64_t * intPointer; 
: 
// Init intPointer somehow 
: 
double * doublePointer = (double *)intPointer; 

Bây giờ doublePointer và intPointer cả điểm đến vị trí bộ nhớ như nhau, nhưng điều trị này là cùng loại. Đây là tình huống bạn nên giải quyết với một công đoàn thực sự, bất cứ điều gì khác là khá xấu. Xấu không phải là mã của tôi!

Bản sao mã của tôi theo giá trị, không phải bằng tham chiếu. Tôi tạo một con trỏ kép đến int64 (hoặc cách khác tròn) và ngay lập tức deference nó. Một khi các hàm trả về, không có con trỏ nào được giữ lại. Có một int64 và một đôi và chúng hoàn toàn không liên quan đến tham số đầu vào của các hàm. Tôi không bao giờ sao chép bất kỳ con trỏ nào đến con trỏ của một loại khác (nếu bạn thấy điều này trong mẫu mã của tôi, bạn đã đọc sai mã C mà tôi đã viết), tôi chỉ chuyển giá trị sang biến khác (trong vị trí bộ nhớ riêng) . Vì vậy, định nghĩa của loại punning không áp dụng ở tất cả, vì nó nói "tham khảo cùng một vị trí trong bộ nhớ" và không có gì ở đây đề cập đến cùng một vị trí bộ nhớ.

int64_t intValue = 12345; 
double doubleValue = int64ToDouble(intValue); 
// The statement below will not change the value of doubleValue! 
// Both are not pointing to the same memory location, both have their 
// own storage space on stack and are totally unreleated. 
intValue = 5678; 

Mã của tôi không có gì khác ngoài bản sao bộ nhớ, chỉ được viết bằng C mà không có chức năng bên ngoài.

int64_t doubleToInt64(double d) 
{ 
    return *(int64_t *)&d; 
} 

thể được viết như

int64_t doubleToInt64(double d) 
{ 
    int64_t result; 
    memcpy(&result, &d, sizeof(d)); 
    return result; 
} 

Không có gì hơn thế nữa, vì vậy không có loại punning ngay cả trong cảnh bất cứ nơi nào. Và hoạt động này cũng hoàn toàn an toàn, an toàn như một hoạt động có thể ở C. Một đôi được định nghĩa luôn là 64 Bit (không giống như int nó không thay đổi về kích thước, nó được cố định ở 64 bit), do đó nó sẽ luôn luôn phù hợp vào một biến kích thước int64_t.

+0

Trên điểm đầu tiên của bạn, chương trình đọc dữ liệu được tạo bởi chương trình khác. Về điểm thứ hai, điều này dường như được cau mày trên: http://cocoawithlove.com/2008/04/using-pointers-to-recast-in-c-is-bad.html và một phần của những gì tôi đang hỏi là liệu tôi có nên tránh xa nó hoàn toàn không. –

+0

Xem cập nhật ở trên. Không có loại xảo quyệt nào liên quan đến việc xác nhận quyền sở hữu trang được liên kết của bạn, thậm chí không gần. Và không giống như mã của bạn, tôi cũng không bao giờ bỏ con trỏ char vào bất cứ thứ gì (vì điều này là không an toàn!), Tôi chuyển tất cả dữ liệu theo giá trị (không bao giờ bằng tham chiếu!) Và tôi chỉ chọn giữa các loại được đảm bảo có cùng kích thước – Mecki

+0

Truyền theo giá trị không thể trong mã của tôi vì lý do hiệu suất. Tôi không bao giờ đúc một con trỏ char; Tôi đúc nội dung của nó. Cuối cùng, ntoh * chỉ hoạt động khi truyền các giá trị lớn. Không có hàm tương ứng cho các giá trị nhỏ. –

Các vấn đề liên quan