Làm thế nào để phân bổ bộ nhớ liên kết chỉ sử dụng thư viện tiêu chuẩn?


422

Tôi vừa hoàn thành một bài kiểm tra như một phần của một cuộc phỏng vấn xin việc, và một câu hỏi làm tôi bối rối, thậm chí sử dụng Google để tham khảo. Tôi muốn xem nhóm StackOverflow có thể làm gì với nó:

Các memset_16alignedchức năng đòi hỏi một 16-byte aligned con trỏ được truyền cho nó, hoặc nó sẽ sụp đổ.

a) Làm thế nào bạn có thể phân bổ 1024 byte bộ nhớ và căn chỉnh nó thành ranh giới 16 byte?
b) Giải phóng bộ nhớ sau khi memset_16alignedđã thực hiện.

{    
   void *mem;
   void *ptr;

   // answer a) here

   memset_16aligned(ptr, 0, 1024);

   // answer b) here    
}

89
hmmm ... đối với khả năng tồn tại mã dài hạn, làm thế nào về "Lửa bất cứ ai đã viết memset_16align và sửa nó hoặc thay thế nó để nó không có điều kiện ranh giới đặc biệt"
Steven A. Lowe

29
Chắc chắn là một câu hỏi hợp lệ để hỏi - "tại sao căn chỉnh bộ nhớ đặc biệt". Nhưng có thể có lý do chính đáng cho nó - trong trường hợp này, có thể là memset_16align () có thể sử dụng số nguyên 128 bit và điều này sẽ dễ dàng hơn nếu bộ nhớ được căn chỉnh. V.v.
Jonathan Leffler

5
Bất cứ ai đã viết memset đều có thể sử dụng căn chỉnh 16 byte bên trong để xóa vòng lặp bên trong và một prolog / epilog dữ liệu nhỏ để dọn sạch các đầu không liên kết. Điều đó sẽ dễ dàng hơn nhiều so với việc làm cho các lập trình viên xử lý các con trỏ bộ nhớ thêm.
Adisak

8
Tại sao ai đó muốn dữ liệu được căn chỉnh theo ranh giới 16 byte? Có lẽ để tải nó vào các thanh ghi SSE 128 bit. Tôi tin rằng các Mov không được phân bổ (mới hơn) (ví dụ: Movupd, lddqu) chậm hơn hoặc có lẽ chúng đang nhắm mục tiêu các bộ xử lý mà không có SSE2 / 3

11
Căn chỉnh địa chỉ dẫn đến việc sử dụng bộ nhớ cache được tối ưu hóa cũng như băng thông cao hơn giữa các mức bộ nhớ cache và RAM khác nhau (đối với hầu hết các khối lượng công việc phổ biến). Xem tại đây stackoverflow.com/questions/381244/purpose-of-memory-alocation
Deep Dùt

Câu trả lời:


587

Câu trả lời gốc

{
    void *mem = malloc(1024+16);
    void *ptr = ((char *)mem+16) & ~ 0x0F;
    memset_16aligned(ptr, 0, 1024);
    free(mem);
}

Trả lời cố định

{
    void *mem = malloc(1024+15);
    void *ptr = ((uintptr_t)mem+15) & ~ (uintptr_t)0x0F;
    memset_16aligned(ptr, 0, 1024);
    free(mem);
}

Giải thích theo yêu cầu

Bước đầu tiên là phân bổ đủ không gian dự phòng, chỉ trong trường hợp. Vì bộ nhớ phải được căn chỉnh 16 byte (có nghĩa là địa chỉ byte hàng đầu cần phải là bội số của 16), nên thêm 16 byte đảm bảo rằng chúng ta có đủ không gian. Ở đâu đó trong 16 byte đầu tiên, có một con trỏ được căn chỉnh 16 byte. (Lưu ý rằng malloc()có nghĩa vụ phải trả về một con trỏ được đầy đủ cũng liên kết với bất kỳ . Mục đích Tuy nhiên, ý nghĩa của 'bất kỳ' là chủ yếu cho những thứ như cơ bản loại - long, double, long double, long long., Và con trỏ đến đối tượng và con trỏ đến chức năng Khi bạn đang ở làm những việc chuyên biệt hơn, như chơi với các hệ thống đồ họa, chúng có thể cần sự liên kết chặt chẽ hơn so với phần còn lại của hệ thống - do đó các câu hỏi và câu trả lời như thế này.)

Bước tiếp theo là chuyển đổi con trỏ void thành con trỏ char; Mặc dù vậy, GCC không được phép thực hiện số học con trỏ trên các con trỏ void (và GCC có các tùy chọn cảnh báo để cho bạn biết khi bạn lạm dụng nó). Sau đó thêm 16 vào con trỏ bắt đầu. Giả sử malloc()trả về cho bạn một con trỏ được căn chỉnh cực kỳ tệ: 0x800001. Thêm 16 cho 0x800011. Bây giờ tôi muốn làm tròn xuống ranh giới 16 byte - vì vậy tôi muốn đặt lại 4 bit cuối cùng thành 0. 0x0F có 4 bit cuối cùng được đặt thành một; do đó, ~0x0Fcó tất cả các bit được đặt thành một ngoại trừ bốn bit cuối cùng. Anding rằng với 0x800011 cho 0x800010. Bạn có thể lặp lại các lần bù khác và thấy rằng cùng một số học hoạt động.

Bước cuối cùng, free()là dễ dàng: bạn luôn, và chỉ trở về free()một giá trị mà một trong malloc(), calloc()hoặc realloc()trả lại cho bạn - bất cứ điều gì khác là một thảm họa. Bạn cung cấp chính xác memđể giữ giá trị đó - cảm ơn bạn. Miễn phí phát hành nó.

Cuối cùng, nếu bạn biết về các phần bên trong của mallocgói hệ thống của mình , bạn có thể đoán rằng nó cũng có thể trả về dữ liệu được căn chỉnh 16 byte (hoặc có thể được căn chỉnh 8 byte). Nếu nó được căn chỉnh 16 byte, thì bạn không cần phải làm mờ các giá trị. Tuy nhiên, đây là tinh ranh và không di động - các mallocgói khác có sự sắp xếp tối thiểu khác nhau, và do đó, giả sử một điều khi nó làm một cái gì đó khác nhau sẽ dẫn đến các bãi rác cốt lõi. Trong giới hạn rộng, giải pháp này là di động.

Một số người khác được đề cập posix_memalign()như một cách khác để có được bộ nhớ phù hợp; không có sẵn ở mọi nơi, nhưng thường có thể được thực hiện bằng cách sử dụng điều này làm cơ sở. Lưu ý rằng thật thuận tiện khi căn chỉnh là lũy thừa 2; sắp xếp khác là lộn xộn hơn.

Thêm một nhận xét - mã này không kiểm tra việc phân bổ đã thành công.

Sửa đổi

Lập trình viên Windows chỉ ra rằng bạn không thể thực hiện các thao tác mặt nạ bit trên con trỏ và thực tế, GCC (đã kiểm tra 3.4.6 và 4.3.1) đã phàn nàn như vậy. Vì vậy, một phiên bản sửa đổi của mã cơ bản - được chuyển đổi thành một chương trình chính, theo sau. Tôi cũng có quyền tự do chỉ thêm 15 thay vì 16, như đã được chỉ ra. Tôi đang sử dụng uintptr_tvì C99 đã có khoảng thời gian đủ dài để có thể truy cập trên hầu hết các nền tảng. Nếu nó không được sử dụng PRIXPTRtrong các printf()báo cáo, nó sẽ đủ để #include <stdint.h>thay vì sử dụng #include <inttypes.h>. [Mã này bao gồm bản sửa lỗi được chỉ ra bởi CR , được nhắc lại một điểm đầu tiên được thực hiện bởi Bill K một số năm trước, mà tôi đã bỏ qua cho đến bây giờ.]

#include <assert.h>
#include <inttypes.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

static void memset_16aligned(void *space, char byte, size_t nbytes)
{
    assert((nbytes & 0x0F) == 0);
    assert(((uintptr_t)space & 0x0F) == 0);
    memset(space, byte, nbytes);  // Not a custom implementation of memset()
}

int main(void)
{
    void *mem = malloc(1024+15);
    void *ptr = (void *)(((uintptr_t)mem+15) & ~ (uintptr_t)0x0F);
    printf("0x%08" PRIXPTR ", 0x%08" PRIXPTR "\n", (uintptr_t)mem, (uintptr_t)ptr);
    memset_16aligned(ptr, 0, 1024);
    free(mem);
    return(0);
}

Và đây là một phiên bản tổng quát hơn một chút, sẽ hoạt động với các kích thước có sức mạnh bằng 2:

#include <assert.h>
#include <inttypes.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

static void memset_16aligned(void *space, char byte, size_t nbytes)
{
    assert((nbytes & 0x0F) == 0);
    assert(((uintptr_t)space & 0x0F) == 0);
    memset(space, byte, nbytes);  // Not a custom implementation of memset()
}

static void test_mask(size_t align)
{
    uintptr_t mask = ~(uintptr_t)(align - 1);
    void *mem = malloc(1024+align-1);
    void *ptr = (void *)(((uintptr_t)mem+align-1) & mask);
    assert((align & (align - 1)) == 0);
    printf("0x%08" PRIXPTR ", 0x%08" PRIXPTR "\n", (uintptr_t)mem, (uintptr_t)ptr);
    memset_16aligned(ptr, 0, 1024);
    free(mem);
}

int main(void)
{
    test_mask(16);
    test_mask(32);
    test_mask(64);
    test_mask(128);
    return(0);
}

Để chuyển đổi test_mask()thành hàm phân bổ mục đích chung, giá trị trả về duy nhất từ ​​bộ cấp phát sẽ phải mã hóa địa chỉ phát hành, như một số người đã chỉ ra trong câu trả lời của họ.

Vấn đề với người phỏng vấn

Uri nhận xét: Có thể tôi đang gặp [a] vấn đề đọc hiểu sáng nay, nhưng nếu câu hỏi phỏng vấn nói cụ thể: "Làm thế nào bạn sẽ phân bổ 1024 byte bộ nhớ" và bạn phân bổ rõ ràng hơn thế. Đó sẽ không phải là một thất bại tự động từ người phỏng vấn?

Phản hồi của tôi sẽ không phù hợp với nhận xét 300 ký tự ...

Nó phụ thuộc, tôi cho rằng. Tôi nghĩ rằng hầu hết mọi người (bao gồm cả tôi) đã đặt câu hỏi có nghĩa là "Làm thế nào bạn có thể phân bổ một không gian trong đó 1024 byte dữ liệu có thể được lưu trữ và trong đó địa chỉ cơ sở là bội số của 16 byte". Nếu người phỏng vấn thực sự có nghĩa là làm thế nào bạn có thể phân bổ 1024 byte (chỉ) và có 16 byte được căn chỉnh, thì các tùy chọn bị hạn chế hơn.

  • Rõ ràng, một khả năng là phân bổ 1024 byte và sau đó cung cấp cho địa chỉ đó 'điều trị căn chỉnh'; vấn đề với cách tiếp cận đó là không gian có sẵn thực tế không được xác định đúng (không gian có thể sử dụng nằm trong khoảng từ 1008 đến 1024 byte, nhưng không có cơ chế có sẵn để chỉ định kích thước nào), khiến nó không hữu dụng.
  • Một khả năng khác là bạn dự kiến ​​sẽ viết một bộ cấp phát bộ nhớ đầy đủ và đảm bảo rằng khối 1024 byte mà bạn trả về được căn chỉnh phù hợp. Nếu đó là trường hợp, có lẽ bạn sẽ thực hiện một thao tác khá giống với những gì giải pháp đề xuất đã làm, nhưng bạn ẩn nó bên trong bộ cấp phát.

Tuy nhiên, nếu người phỏng vấn mong đợi một trong những câu trả lời đó, tôi mong họ nhận ra rằng giải pháp này trả lời một câu hỏi liên quan chặt chẽ, và sau đó điều chỉnh lại câu hỏi của họ để chỉ cuộc trò chuyện theo đúng hướng. (Hơn nữa, nếu người phỏng vấn thực sự khó tính, thì tôi sẽ không muốn công việc; nếu câu trả lời cho một yêu cầu không chính xác bị bắn hạ trong ngọn lửa mà không sửa, thì người phỏng vấn không phải là người an toàn để làm việc.)

Thế giới chuyển sang

Tiêu đề của câu hỏi đã thay đổi gần đây. Đó là giải quyết sự liên kết bộ nhớ trong câu hỏi phỏng vấn C làm tôi bối rối . Tiêu đề sửa đổi ( Cách phân bổ bộ nhớ được căn chỉnh chỉ bằng thư viện chuẩn? ) Yêu cầu một câu trả lời được sửa đổi một chút - phụ lục này cung cấp nó.

Chức năng được thêm vào C11 (ISO / IEC 9899: 2011) aligned_alloc():

7.22.3.1 aligned_allocHàm

Tóm tắc

#include <stdlib.h>
void *aligned_alloc(size_t alignment, size_t size);

Mô tả
Các aligned_allockhông gian chức năng giao đất cho một đối tượng có liên kết được xác định bởi alignment, có kích thước được xác định bởi size, và có giá trị là không xác định. Giá trị của alignmentsẽ là một căn chỉnh hợp lệ được hỗ trợ bởi việc thực hiện và giá trị của sizesẽ là một bội số của alignment.

Trả
Các aligned_allocchức năng lợi nhuận hoặc một con trỏ null hoặc một con trỏ để không gian được phân bổ.

Và POSIX định nghĩa posix_memalign():

#include <stdlib.h>

int posix_memalign(void **memptr, size_t alignment, size_t size);

SỰ MIÊU TẢ

Các posix_memalign()chức năng có trách nhiệm phân bổ sizebyte xếp trên một ranh giới được xác định bởi alignment, và sẽ trả về một con trỏ tới bộ nhớ phân bổ memptr. Giá trị của alignmentsẽ là một lũy thừa của hai bội số sizeof(void *).

Sau khi hoàn thành thành công, giá trị được trỏ đến memptrsẽ là bội số của alignment.

Nếu kích thước của không gian được yêu cầu là 0, thì hành vi được xác định theo thực hiện; giá trị được trả về memptrsẽ là con trỏ null hoặc con trỏ duy nhất.

Các free()chức năng có trách nhiệm deallocate nhớ rằng trước đây đã được giao posix_memalign().

GIÁ TRỊ TRẢ LẠI

Sau khi hoàn thành, posix_memalign()sẽ trả về 0; mặt khác, một số lỗi sẽ được trả về để chỉ ra lỗi.

Bây giờ hoặc cả hai đều có thể được sử dụng để trả lời câu hỏi, nhưng chỉ có chức năng POSIX là một tùy chọn khi câu hỏi ban đầu được trả lời.

Đằng sau hậu trường, chức năng bộ nhớ căn chỉnh mới thực hiện nhiều công việc tương tự như được nêu trong câu hỏi, ngoại trừ chúng có khả năng buộc căn chỉnh dễ dàng hơn và theo dõi sự khởi đầu của bộ nhớ được căn chỉnh bên trong để mã không phải xử lý đặc biệt - nó chỉ giải phóng bộ nhớ được trả về bởi hàm phân bổ đã được sử dụng.


13
Và tôi rất cuồng với C ++, nhưng tôi không thực sự tin tưởng rằng ~ 0x0F sẽ mở rộng đúng kích thước của con trỏ. Nếu không, tất cả địa ngục sẽ vỡ ra vì bạn cũng sẽ che giấu các bit quan trọng nhất của con trỏ. Tôi có thể sai về điều đó mặc dù.
Bill K

66
BTW '+15' hoạt động cũng như '+16' ... không có tác động thực tế nào trong tình huống này.
Menkboy

15
Nhận xét '+ 15' từ Menkboy và Greg là chính xác, nhưng malloc () gần như chắc chắn sẽ làm tròn đến 16 dù sao đi nữa. Sử dụng +16 là dễ dàng hơn để giải thích. Các giải pháp tổng quát là khó khăn, nhưng có thể làm được.
Jonathan Leffler

6
@Aerovistae: Đây là một câu hỏi mẹo nhỏ và chủ yếu dựa vào sự hiểu biết của bạn về cách tạo một số tùy ý (thực sự là địa chỉ được trả về bởi bộ cấp phát bộ nhớ) phù hợp với một yêu cầu nhất định (bội số của 16). Nếu bạn được yêu cầu làm tròn số 53 đến bội số gần nhất của 16, bạn sẽ làm thế nào? Quá trình không khác nhau cho các địa chỉ; chỉ là những con số bạn thường xử lý lớn hơn. Đừng quên, các câu hỏi phỏng vấn được yêu cầu để tìm hiểu suy nghĩ của bạn, không tìm hiểu xem bạn có biết câu trả lời hay không.
Jonathan Leffler

3
@akristmann: Mã ban đầu là chính xác nếu bạn có <inttypes.h>sẵn từ C99 (ít nhất là đối với chuỗi định dạng - có thể nói, các giá trị phải được truyền bằng một biểu tượng (uintptr_t)mem, (uintptr_t)ptr:). Chuỗi định dạng dựa trên nối chuỗi và macro PRIXPTR là bộ xác định printf()độ dài và loại chính xác cho đầu ra hex cho một uintptr_tgiá trị. Cách khác là sử dụng %pnhưng đầu ra từ đó thay đổi theo nền tảng (một số thêm hàng đầu 0x, hầu hết không) và thường được viết bằng các chữ số hex chữ thường, mà tôi không thích; những gì tôi đã viết là thống nhất trên các nền tảng.
Jonathan Leffler

58

Ba câu trả lời hơi khác nhau tùy thuộc vào cách bạn nhìn vào câu hỏi:

1) Đủ tốt cho câu hỏi chính xác được hỏi là giải pháp của Jonathan Leffler, ngoại trừ việc làm tròn đến 16 liên kết, bạn chỉ cần thêm 15 byte, không phải 16.

A:

/* allocate a buffer with room to add 0-15 bytes to ensure 16-alignment */
void *mem = malloc(1024+15);
ASSERT(mem); // some kind of error-handling code
/* round up to multiple of 16: add 15 and then round down by masking */
void *ptr = ((char*)mem+15) & ~ (size_t)0x0F;

B:

free(mem);

2) Đối với chức năng cấp phát bộ nhớ chung hơn, người gọi không muốn phải theo dõi hai con trỏ (một để sử dụng và một để giải phóng). Vì vậy, bạn lưu trữ một con trỏ đến bộ đệm 'thực' bên dưới bộ đệm được căn chỉnh.

A:

void *mem = malloc(1024+15+sizeof(void*));
if (!mem) return mem;
void *ptr = ((char*)mem+sizeof(void*)+15) & ~ (size_t)0x0F;
((void**)ptr)[-1] = mem;
return ptr;

B:

if (ptr) free(((void**)ptr)[-1]);

Lưu ý rằng không giống như (1), khi chỉ thêm 15 byte vào mem, mã này thực sự có thể giảm căn chỉnh nếu việc triển khai của bạn xảy ra để đảm bảo căn chỉnh 32 byte từ malloc (không thể, nhưng về mặt lý thuyết, việc triển khai C có thể có 32 byte loại phù hợp). Điều đó không quan trọng nếu tất cả những gì bạn làm là gọi memset_16align, nhưng nếu bạn sử dụng bộ nhớ cho một cấu trúc thì nó có thể quan trọng.

Tôi không chắc chắn cách khắc phục tốt cho việc này (ngoài việc cảnh báo người dùng rằng bộ đệm được trả lại không nhất thiết phải phù hợp với các cấu trúc tùy ý) vì không có cách nào để xác định bảo đảm căn chỉnh cụ thể theo cách lập trình là gì. Tôi đoán khi khởi động, bạn có thể phân bổ hai hoặc nhiều bộ đệm 1 byte và giả sử rằng căn chỉnh tệ nhất mà bạn thấy là căn chỉnh được đảm bảo. Nếu bạn sai, bạn lãng phí bộ nhớ. Bất cứ ai có ý tưởng tốt hơn, xin vui lòng nói như vậy ...

[ Đã thêm : Thủ thuật 'tiêu chuẩn' là tạo ra một liên kết 'có khả năng được sắp xếp tối đa các loại' để xác định căn chỉnh cần thiết. Các loại được căn chỉnh tối đa có thể là (trong C99) ' long long', ' long double', ' void *' hoặc ' void (*)(void)'; nếu bạn bao gồm <stdint.h>, có lẽ bạn có thể sử dụng ' intmax_t' thay cho long long(và, trên máy Power 6 (AIX), intmax_tsẽ cung cấp cho bạn loại số nguyên 128 bit). Các yêu cầu căn chỉnh cho liên kết đó có thể được xác định bằng cách nhúng nó vào một cấu trúc với một char duy nhất theo sau bởi liên minh:

struct alignment
{
    char     c;
    union
    {
        intmax_t      imax;
        long double   ldbl;
        void         *vptr;
        void        (*fptr)(void);
    }        u;
} align_data;
size_t align = (char *)&align_data.u.imax - &align_data.c;

Sau đó, bạn sẽ sử dụng mức lớn hơn của căn chỉnh được yêu cầu (trong ví dụ 16) và aligngiá trị được tính ở trên.

Trên (64-bit) Solaris 10, có vẻ như căn chỉnh cơ bản cho kết quả từ malloc()là bội số của 32 byte.
]

Trong thực tế, các bộ cấp phát căn chỉnh thường lấy một tham số cho căn chỉnh thay vì nó được gắn kết. Vì vậy, người dùng sẽ vượt qua kích thước của cấu trúc mà họ quan tâm (hoặc công suất nhỏ nhất bằng 2 lớn hơn hoặc bằng mức đó) và tất cả sẽ ổn.

3) Sử dụng những gì nền tảng của bạn cung cấp: posix_memaligncho POSIX, _aligned_malloctrên Windows.

4) Nếu bạn sử dụng C11, thì tùy chọn sạch nhất - di động và súc tích - là sử dụng chức năng thư viện chuẩn aligned_allocđược giới thiệu trong phiên bản đặc tả ngôn ngữ này.


1
Tôi đồng ý - Tôi nghĩ rằng mục đích của câu hỏi là mã giải phóng khối bộ nhớ sẽ chỉ có quyền truy cập vào con trỏ được căn chỉnh 16 byte 'nấu chín'.
Michael Burr

1
Đối với một giải pháp chung - bạn đúng. Tuy nhiên, mẫu mã trong câu hỏi cho thấy rõ cả hai.
Jonathan Leffler

1
Chắc chắn, và trong một cuộc phỏng vấn tốt, điều gì xảy ra là bạn đưa ra câu trả lời của mình, sau đó nếu người phỏng vấn muốn xem câu trả lời của tôi, họ sẽ thay đổi câu hỏi.
Steve Jessop

1
Tôi phản đối việc sử dụng ASSERT(mem);để kiểm tra kết quả phân bổ; assertlà để bắt lỗi lập trình và không thiếu tài nguyên thời gian chạy.
hlovdal

4
Sử dụng nhị phân & với a char *và a size_tsẽ dẫn đến lỗi. Bạn sẽ phải sử dụng một cái gì đó như uintptr_t.
Marko


20

Đây là một cách tiếp cận thay thế cho phần 'làm tròn lên'. Không phải là giải pháp được mã hóa mạnh mẽ nhất nhưng nó hoàn thành công việc và loại cú pháp này dễ nhớ hơn một chút (cộng với sẽ hoạt động đối với các giá trị căn chỉnh không phải là lũy thừa 2). Các uintptr_tdiễn viên là cần thiết để xoa dịu trình biên dịch; số học con trỏ không thích chia hoặc nhân.

void *mem = malloc(1024 + 15);
void *ptr = (void*) ((uintptr_t) mem + 15) / 16 * 16;
memset_16aligned(ptr, 0, 1024);
free(mem);

2
Nói chung, trong trường hợp bạn có 'dài không dấu', bạn cũng có uintptr_t được xác định rõ ràng là đủ lớn để giữ một con trỏ dữ liệu (void *). Nhưng giải pháp của bạn thực sự có giá trị nếu, vì một số lý do, bạn cần một sự liên kết không phải là sức mạnh của 2. Không thể, nhưng có thể.
Jonathan Leffler

@Andrew: Được nâng cấp cho loại cú pháp này dễ nhớ hơn một chút (cộng với sẽ hoạt động đối với các giá trị căn chỉnh không phải là lũy thừa 2) .
huyền thoại2k

19

Thật không may, trong C99, có vẻ khá khó khăn để đảm bảo sự liên kết của bất kỳ loại nào theo cách có thể di chuyển trên bất kỳ triển khai C nào phù hợp với C99. Tại sao? Bởi vì một con trỏ không được đảm bảo là "địa chỉ byte" mà người ta có thể tưởng tượng với một mô hình bộ nhớ phẳng. Không phải là đại diện của uintptr_t cũng được đảm bảo, dù sao nó cũng là một loại tùy chọn.

Chúng ta có thể biết một số triển khai sử dụng biểu diễn cho void * (và theo định nghĩa, cũng là char * ) là một địa chỉ byte đơn giản, nhưng bởi C99, nó là mờ đối với chúng ta, các lập trình viên. Việc triển khai có thể biểu thị một con trỏ theo tập { phân đoạn , offset } trong đó offset có thể có sự liên kết ai biết "trong thực tế". Tại sao, một con trỏ thậm chí có thể là một dạng giá trị tra cứu bảng băm hoặc thậm chí là giá trị tra cứu danh sách liên kết. Nó có thể mã hóa giới hạn thông tin.

Trong một bản nháp C1X gần đây cho Tiêu chuẩn C, chúng ta thấy từ khóa _Alignas . Điều đó có thể giúp một chút.

Bảo đảm duy nhất mà C99 mang lại cho chúng ta là các hàm cấp phát bộ nhớ sẽ trả về một con trỏ phù hợp để gán cho một con trỏ trỏ vào bất kỳ loại đối tượng nào. Vì chúng tôi không thể chỉ định căn chỉnh của các đối tượng, chúng tôi không thể thực hiện các chức năng phân bổ của riêng mình với trách nhiệm căn chỉnh theo cách di động được xác định rõ ràng.

Sẽ là tốt để sai về yêu cầu này.


C11 có aligned_alloc(). (C ++ 11/14 / 1z vẫn không có nó). _Alignas()và C ++ alignas()không làm bất cứ điều gì để phân bổ động, chỉ dành cho lưu trữ tự động và tĩnh (hoặc bố cục cấu trúc).
Peter Cordes

15

Trên mặt trước đệm số đếm 16 so với 15 byte, số thực tế bạn cần thêm để có được căn chỉnh là N là tối đa (0, NM) trong đó M là căn chỉnh tự nhiên của bộ cấp phát bộ nhớ (và cả hai đều là lũy thừa của 2).

Vì căn chỉnh bộ nhớ tối thiểu của bất kỳ bộ cấp phát nào là 1 byte, 15 = max (0,16-1) là một câu trả lời bảo thủ. Tuy nhiên, nếu bạn biết bộ cấp phát bộ nhớ của bạn sẽ cung cấp cho bạn các địa chỉ int được liên kết 32 bit (khá phổ biến), bạn có thể đã sử dụng 12 làm bảng đệm.

Điều này không quan trọng trong ví dụ này nhưng nó có thể quan trọng trên một hệ thống nhúng có 12K RAM trong đó mỗi lần lưu int được lưu.

Cách tốt nhất để thực hiện nó nếu bạn thực sự cố gắng lưu mọi byte có thể là dưới dạng macro để bạn có thể cung cấp cho nó căn chỉnh bộ nhớ riêng. Một lần nữa, điều này có lẽ chỉ hữu ích cho các hệ thống nhúng, nơi bạn cần lưu từng byte.

Trong ví dụ dưới đây, trên hầu hết các hệ thống, giá trị 1 chỉ phù hợp MEMORY_ALLOCATOR_NATIVE_ALIGNMENT, tuy nhiên đối với hệ thống nhúng lý thuyết của chúng tôi với phân bổ được căn chỉnh 32 bit, những điều sau đây có thể tiết kiệm một chút bộ nhớ quý giá:

#define MEMORY_ALLOCATOR_NATIVE_ALIGNMENT    4
#define ALIGN_PAD2(N,M) (((N)>(M)) ? ((N)-(M)) : 0)
#define ALIGN_PAD(N) ALIGN_PAD2((N), MEMORY_ALLOCATOR_NATIVE_ALIGNMENT)

8

Có lẽ họ sẽ hài lòng với một kiến ​​thức về memalign ? Và như Jonathan Leffler chỉ ra, có hai chức năng thích hợp mới hơn để biết.

Rất tiếc, florin đã đánh bại tôi. Tuy nhiên, nếu bạn đọc trang người đàn ông tôi liên kết đến, rất có thể bạn sẽ hiểu ví dụ được cung cấp bởi một poster trước đó.


1
Lưu ý rằng phiên bản hiện tại (tháng 2 năm 2016) của trang được tham chiếu cho biết " memalignHàm này đã lỗi thời và aligned_allochoặc posix_memalignnên được sử dụng thay thế". Tôi không biết những gì nó nói vào tháng 10 năm 2008 - nhưng có lẽ nó đã không được đề cập aligned_alloc()vì nó đã được thêm vào C11.
Jonathan Leffler

5

Chúng tôi làm điều này mọi lúc mọi nơi cho Accelerate.framework, một thư viện OS X / iOS được vector hóa mạnh mẽ, nơi chúng tôi phải chú ý đến việc căn chỉnh mọi lúc. Có khá nhiều lựa chọn, một hoặc hai trong số đó tôi không thấy được đề cập ở trên.

Phương pháp nhanh nhất cho một mảng nhỏ như thế này chỉ là dán nó vào ngăn xếp. Với GCC / tiếng kêu:

 void my_func( void )
 {
     uint8_t array[1024] __attribute__ ((aligned(16)));
     ...
 }

Không yêu cầu () miễn phí. Đây thường là hai hướng dẫn: trừ 1024 từ con trỏ ngăn xếp, sau đó VÀ con trỏ ngăn xếp với -align. Có lẽ người yêu cầu cần dữ liệu trên heap vì tuổi thọ của mảng vượt quá ngăn xếp hoặc đệ quy là tại nơi làm việc hoặc không gian ngăn xếp ở mức cao.

Trên OS X / iOS, tất cả các cuộc gọi đến malloc / calloc / etc. luôn được căn chỉnh 16 byte. Ví dụ: nếu bạn cần 32 byte được căn chỉnh cho AVX, thì bạn có thể sử dụng posix_memalign:

void *buf = NULL;
int err = posix_memalign( &buf, 32 /*alignment*/, 1024 /*size*/);
if( err )
   RunInCirclesWaivingArmsWildly();
...
free(buf);

Một số người đã đề cập đến giao diện C ++ hoạt động tương tự.

Không nên quên rằng các trang được căn chỉnh theo sức mạnh lớn của hai, vì vậy bộ đệm căn chỉnh trang cũng được căn chỉnh 16 byte. Do đó, mmap () và valloc () và các giao diện tương tự khác cũng là các tùy chọn. mmap () có lợi thế là bộ đệm có thể được phân bổ trước với một cái gì đó khác không, nếu bạn muốn. Vì các trang này có kích thước được căn chỉnh trang, bạn sẽ không nhận được phân bổ tối thiểu từ các trang này và có thể nó sẽ bị lỗi VM trong lần đầu tiên bạn chạm vào nó.

Cheesy: Bật bảo vệ malloc hoặc tương tự. Các bộ đệm có kích thước n * 16 byte như cái này sẽ được n * 16 byte được căn chỉnh, bởi vì VM được sử dụng để bắt tràn và các ranh giới của nó nằm ở ranh giới trang.

Một số chức năng Accelerate.framework lấy bộ đệm tạm thời do người dùng cung cấp để sử dụng làm không gian đầu. Ở đây chúng ta phải giả định rằng bộ đệm được truyền cho chúng ta rất sai lệch và người dùng đang tích cực cố gắng để làm cho cuộc sống của chúng ta khó khăn. (Các trường hợp thử nghiệm của chúng tôi dán một trang bảo vệ ngay trước và sau bộ đệm tạm thời để gạch chân spite.) Ở đây, chúng tôi trả lại kích thước tối thiểu mà chúng tôi cần để đảm bảo phân đoạn được liên kết 16 byte ở đâu đó trong đó, sau đó căn chỉnh thủ công bộ đệm sau đó. Kích thước này là mong muốn_size + căn chỉnh - 1. Vì vậy, trong trường hợp này là 1024 + 16 - 1 = 1039 byte. Sau đó căn chỉnh như vậy:

#include <stdint.h>
void My_func( uint8_t *tempBuf, ... )
{
    uint8_t *alignedBuf = (uint8_t*) 
                          (((uintptr_t) tempBuf + ((uintptr_t)alignment-1)) 
                                        & -((uintptr_t) alignment));
    ...
}

Thêm căn chỉnh-1 sẽ di chuyển con trỏ qua địa chỉ được căn chỉnh đầu tiên và sau đó ANDing với -align (ví dụ 0xfff ... ff0 cho căn chỉnh = 16) đưa nó trở lại địa chỉ được căn chỉnh.

Như được mô tả bởi các bài đăng khác, trên các hệ điều hành khác không có bảo đảm căn chỉnh 16 byte, bạn có thể gọi malloc với kích thước lớn hơn, đặt con trỏ miễn phí () sau đó, sau đó căn chỉnh như mô tả ở trên và sử dụng con trỏ được căn chỉnh, nhiều như mô tả cho trường hợp bộ đệm tạm thời của chúng tôi.

Đối với căn_themset, điều này khá ngớ ngẩn. Bạn chỉ phải lặp tối đa 15 byte để đến địa chỉ được căn chỉnh và sau đó tiến hành các cửa hàng được căn chỉnh sau đó với một số mã dọn dẹp có thể ở cuối. Bạn thậm chí có thể thực hiện các bit dọn dẹp trong mã vectơ, dưới dạng các cửa hàng không được xếp chồng lên vùng được căn chỉnh (cung cấp độ dài ít nhất là chiều dài của vectơ) hoặc sử dụng cái gì đó như Movmaskdqu. Có người chỉ lười biếng. Tuy nhiên, đây có thể là một câu hỏi phỏng vấn hợp lý nếu người phỏng vấn muốn biết liệu bạn có cảm thấy thoải mái với stdint.h, các toán tử bitwise và các nguyên tắc cơ bản của bộ nhớ hay không, vì vậy ví dụ giả định có thể được tha thứ.


5

Tôi ngạc nhiên khi không ai bình chọn câu trả lời của Shao rằng, theo tôi hiểu, không thể thực hiện những gì được hỏi trong tiêu chuẩn C99, vì việc chuyển đổi một con trỏ thành một loại tích phân chính thức là hành vi không xác định. (Ngoài tiêu chuẩn cho phép chuyển đổi <-> , nhưng tiêu chuẩn dường như không cho phép thực hiện bất kỳ thao tác nào của giá trị và sau đó chuyển đổi lại.)uintptr_tvoid*uintptr_t


Không có yêu cầu rằng loại uintptr_t tồn tại hoặc các bit của nó có bất kỳ mối quan hệ nào với các bit trong con trỏ bên dưới. Nếu ai đó phân bổ quá mức lưu trữ, hãy lưu trữ con trỏ dưới dạng unsigned char* myptr; và sau đó tính toán `mptr + = (16- (uintptr_t) my_ptr) & 0x0F, hành vi sẽ được xác định trên tất cả các triển khai xác định my_ptr, nhưng liệu con trỏ kết quả có được căn chỉnh hay không sẽ phụ thuộc vào ánh xạ giữa các bit và địa chỉ uintptr_t.
supercat


3

Điều đầu tiên xuất hiện trong đầu tôi khi đọc câu hỏi này là xác định một cấu trúc được căn chỉnh, khởi tạo nó và sau đó chỉ vào nó.

Có một lý do cơ bản mà tôi mất tích vì không ai đề xuất điều này?

Là một sidenote, vì tôi đã sử dụng một mảng char (giả sử char của hệ thống là 8 bit (tức là 1 byte)), tôi không thấy sự cần thiết cho __attribute__((packed)) thiết (phải sửa cho tôi nếu tôi sai), nhưng tôi đặt nó trong dù sao đi nữa

Hệ thống này hoạt động trên hai hệ thống mà tôi đã thử, nhưng có thể có một tối ưu hóa trình biên dịch mà tôi không biết là mang lại cho tôi những thông tin sai lệch về tính hiệu quả của mã. Tôi đã sử dụng gcc 4.9.2trên OSX và gcc 5.2.1trên Ubuntu.

#include <stdio.h>
#include <stdlib.h>

int main ()
{

   void *mem;

   void *ptr;

   // answer a) here
   struct __attribute__((packed)) s_CozyMem {
       char acSpace[16];
   };

   mem = malloc(sizeof(struct s_CozyMem));
   ptr = mem;

   // memset_16aligned(ptr, 0, 1024);

   // Check if it's aligned
   if(((unsigned long)ptr & 15) == 0) printf("Aligned to 16 bytes.\n");
   else printf("Rubbish.\n");

   // answer b) here
   free(mem);

   return 1;
}

1

MacOS X cụ thể:

  1. Tất cả các con trỏ được phân bổ với malloc là 16 byte được căn chỉnh.
  2. C11 được hỗ trợ, vì vậy bạn chỉ cần gọi căn chỉnh_malloc (16, kích thước).

  3. MacOS X chọn mã được tối ưu hóa cho các bộ xử lý riêng lẻ khi khởi động cho memset, memcpy và memmove và mã đó sử dụng các thủ thuật mà bạn chưa bao giờ nghe thấy để làm cho nó nhanh. 99% khả năng bộ nhớ chạy nhanh hơn bất kỳ bộ nhớ viết tay nào16 khiến cho toàn bộ câu hỏi trở nên vô nghĩa.

Nếu bạn muốn một giải pháp di động 100%, trước C11 thì không có. Bởi vì không có cách di động để kiểm tra căn chỉnh của một con trỏ. Nếu nó không phải di động 100%, bạn có thể sử dụng

char* p = malloc (size + 15);
p += (- (unsigned int) p) % 16;

Điều này giả định rằng sự liên kết của một con trỏ được lưu trữ trong các bit thấp nhất khi chuyển đổi một con trỏ thành int unsign. Chuyển đổi sang int unsign mất thông tin và được triển khai xác định, nhưng điều đó không thành vấn đề vì chúng tôi không chuyển đổi kết quả trở lại thành một con trỏ.

Phần khủng khiếp tất nhiên là con trỏ ban đầu phải được lưu ở đâu đó để gọi free () với nó. Vì vậy, tất cả trong tất cả tôi sẽ thực sự nghi ngờ sự khôn ngoan của thiết kế này.


1
Bạn đang tìm thấy aligned_mallocở đâu trong OS X? Tôi đang sử dụng Xcode 6.1 và nó không được xác định ở bất kỳ đâu trong SDK iOS và cũng không được khai báo ở bất kỳ đâu /usr/include/*.
Todd Lehman

Ditto cho XCode 7.2 trên El Capitan (Mac OS X 10.11.3). Hàm C11, trong mọi trường hợp aligned_alloc(), nhưng điều đó cũng không được khai báo. Từ GCC 5.3.0, tôi nhận được các tin nhắn thú vị alig.c:7:15: error: incompatible implicit declaration of built-in function ‘aligned_alloc’ [-Werror]alig.c:7:15: note: include ‘<stdlib.h>’ or provide a declaration of ‘aligned_alloc’. Mã thực sự bao gồm <stdlib.h>, nhưng -std=c11cũng không -std=gnu11thay đổi các thông báo lỗi.
Jonathan Leffler

0

Bạn cũng có thể thêm một số 16 byte và sau đó đẩy ptr ban đầu lên 16 bit được căn chỉnh bằng cách thêm (16-mod) như bên dưới con trỏ:

main(){
void *mem1 = malloc(1024+16);
void *mem = ((char*)mem1)+1; // force misalign ( my computer always aligns)
printf ( " ptr = %p \n ", mem );
void *ptr = ((long)mem+16) & ~ 0x0F;
printf ( " aligned ptr = %p \n ", ptr );

printf (" ptr after adding diff mod %p (same as above ) ", (long)mem1 + (16 -((long)mem1%16)) );


free(mem1);
}

0

Nếu có các ràng buộc đó, bạn không thể lãng phí một byte đơn, thì giải pháp này hoạt động: Lưu ý: Có một trường hợp điều này có thể được thực thi vô hạn: D

   void *mem;  
   void *ptr;
try:
   mem =  malloc(1024);  
   if (mem % 16 != 0) {  
       free(mem);  
       goto try;
   }  
   ptr = mem;  
   memset_16aligned(ptr, 0, 1024);

Có một cơ hội rất tốt là nếu bạn phân bổ và sau đó giải phóng một khối N byte và sau đó yêu cầu một khối N byte khác, thì khối ban đầu sẽ được trả lại. Vì vậy, một vòng lặp vô hạn rất có thể nếu phân bổ đầu tiên không đáp ứng yêu cầu căn chỉnh. Tất nhiên, điều đó tránh lãng phí một byte đơn với chi phí lãng phí rất nhiều chu kỳ CPU.
Jonathan Leffler

Bạn có chắc chắn %toán tử được định nghĩa void*theo một cách có ý nghĩa?
Ajay Brahmakshatriya

0

Đối với giải pháp tôi đã sử dụng khái niệm đệm để căn chỉnh bộ nhớ và không lãng phí bộ nhớ của một byte đơn.

Nếu có các ràng buộc đó, bạn không thể lãng phí một byte đơn. Tất cả các con trỏ được phân bổ với malloc là 16 byte được căn chỉnh.

C11 được hỗ trợ, vì vậy bạn chỉ cần gọi aligned_alloc (16, size).

void *mem = malloc(1024+16);
void *ptr = ((char *)mem+16) & ~ 0x0F;
memset_16aligned(ptr, 0, 1024);
free(mem);

1
Trên nhiều hệ thống 64 bit, con trỏ được trả về malloc()thực sự được căn chỉnh trên ranh giới 16 byte, nhưng không có gì trong bất kỳ tiêu chuẩn nào đảm bảo rằng - đơn giản là nó sẽ được căn chỉnh đầy đủ cho mọi mục đích sử dụng và trên nhiều hệ thống 32 bit được căn chỉnh trên một Ranh giới 8 byte là đủ và đối với một số người, ranh giới 4 byte là đủ.
Jonathan Leffler

0
size =1024;
alignment = 16;
aligned_size = size +(alignment -(size %  alignment));
mem = malloc(aligned_size);
memset_16aligned(mem, 0, 1024);
free(mem);

Hy vọng điều này là thực hiện đơn giản nhất, cho tôi biết ý kiến ​​của bạn.


-3
long add;   
mem = (void*)malloc(1024 +15);
add = (long)mem;
add = add - (add % 16);//align to 16 byte boundary
ptr = (whatever*)(add);

Tôi nghĩ rằng có vấn đề với điều này bởi vì tiện ích bổ sung của bạn sẽ trỏ đến một vị trí không phải là malloc'd - Không chắc cách này hoạt động với bạn.
quả

@Sam Nó nên như vậy add += 16 - (add % 16). (2 - (2 % 16)) == 0.
SS Anne
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.