2010-09-27 25 views
6

Tôi biết rằng nó được cho biết khi truyền một biến của bất kỳ loại tích phân nào như int, double, long double, vv thành một hàm; nó nên được thực hiện bởi giá trị nhưng tôi tò mò rằng từ một điểm lắp ráp (hiệu suất khôn ngoan hoặc không gian khôn ngoan), sẽ không có một tình huống khi đi qua một biến của một loại tích phân với kích thước lớn hơn con trỏ như dài gấp đôi trên nền tảng của tôi có kích thước 8 byte và có kích thước lớn hơn con trỏ có kích thước 4 byte; bằng cách tham chiếu sẽ hiệu quả hơn?sẽ bao giờ đi qua một biến của một loại tích phân để chức năng bằng cách tham khảo được hiệu quả hơn so với giá trị?

Trả lời

3

Nói chung, nếu kích thước từ của máy (và do đó thường là kích thước con trỏ) nhỏ hơn kích thước của số nguyên, sau đó truyền theo tham chiếu sẽ nhanh hơn. Ví dụ, trên máy 32 bit, chuyển loại uint64_t bằng tham chiếu sẽ nhanh hơn một chút so với truyền theo giá trị, vì để truyền theo giá trị liên quan đến việc sao chép số nguyên, yêu cầu hai tải đăng ký. Đi qua tham chiếu chỉ liên quan đến một tải đăng ký.

Bất kể phần lớn nó không có khả năng tạo ra hiệu suất đáng chú ý trừ khi bạn đang gọi hàm như hàng triệu lần trong một vòng lặp chặt chẽ, trong trường hợp đó hàm có thể được gạch chân nếu có thể.

+6

không phải là có thể (? Khả năng) mà tiết kiệm trong đi ngang qua tham khảo sẽ được nhiều hơn bởi công việc phụ trong việc tiếp cận giá trị gián tiếp trong hàm được gọi? Tính hoán đổi của giá trị/ref ngụ ý tham chiếu const ở đây. –

+1

@Steve, có thể. Có thể không phải trong trường hợp hiếm hoi hơn, chẳng hạn như nếu bạn đang truyền số nguyên 64 bit trên máy 16 bit. –

+0

Trên AMD64, chuyển một '__uint128_t' trong hai thanh ghi nguyên có thể sẽ nhanh hơn lưu trữ nó vào một địa phương, tính toán con trỏ tới địa chỉ đó và chuyển (vào một hàm sau đó phải' add' và 'adc' với toán hạng bộ nhớ). Tôi có thể thấy pass-by-ref nhanh hơn nếu người gọi chưa có giá trị trực tiếp trong sổ đăng ký. (ví dụ: 'foo (p-> x)'). Trong một quy ước gọi 32bit với hai arg đầu tiên trong thanh ghi, thì có thể bởi ref là tốt (chỉ có một reg cho một con trỏ thay vì hai cho giá trị), và chắc chắn tốt hơn nếu các giá trị không có trong regs (hai tải/hai đẩy ...) –

5

Đi qua một con trỏ/tham chiếu đến một giá trị số nguyên lớn hơn kích thước con trỏ bản địa cũng có thể là địa phương tối ưu nhưng thật khó để nói nếu nó sẽ là toàn cầu tối ưu. Điều này phần lớn là do việc sử dụng giá trị của callee. Nếu nó thực sự là một số nguyên và được xử lý như vậy bởi callee thì có khả năng là, tại một thời điểm nào đó, giá trị sẽ được nạp vào một hoặc nhiều thanh ghi (để chương trình thực hiện số học trên các giá trị chẳng hạn) chi phí bổ sung trong callee để dereference con trỏ. Nếu callee được biên dịch bởi một trình biên dịch tối ưu hóa, có khả năng trình biên dịch sẽ đơn giản vượt qua việc chia giá trị số nguyên trên hai thanh ghi. Tuy nhiên, nếu callee không thể được inline (nếu đó là mã API của bên thứ ba) thì trình biên dịch không thể thực hiện kiểu nội tuyến này và thực sự truyền một con trỏ có thể hiệu quả hơn, mặc dù bạn không thể tìm thấy thư viện có chức năng mà lấy một số nguyên đi qua tham chiếu trừ khi nó để các callee có thể sửa đổi giá trị của người gọi: mà giới thiệu một bộ khác nhau của các vấn đề.

Thường thì không phải trình biên dịch tối ưu hóa hiện đại sẽ đưa ra quyết định tối ưu khi xem xét tất cả những điều này và thường tốt nhất cho lập trình viên không phải là. Thực tế, điều này có thể dẫn đến ít hơn mã hiệu quả.

Điều hợp lý nhất để làm trong phần lớn các trường hợp là viết mã theo cách truyền đạt tốt nhất ý định của bạn (giá trị truyền theo giá trị "trừ khi đối số là - áp dụng thuật ngữ C# - semantically tham số "out" hoặc "reference") và lo lắng về hiệu quả chỉ khi có một nút cổ chai hiệu suất rõ ràng.

+3

Để xây dựng điều này, đặc biệt là trên x86-64, một vài đối số hàm đầu tiên được truyền qua thanh ghi và không phải ngăn xếp, cung cấp 64 bit cho số nguyên, 80 bit cho dấu phẩy động và thanh ghi SSE 128bit. Và nói chung, các kiểu tích phân sẽ có một đại diện thanh ghi kiến ​​trúc tương ứng cho cả các bộ xử lý và các tham số truyền. – Brian

+0

@Brian: đúng. +1 –

4

Kiểm tra, thử nghiệm, thử nghiệm, tháo rời, tháo rời, tháo rời.

Số nguyên có kích thước gốc, đơn giản.

 
unsigned int fun_one (unsigned int a) 
{ 
    return((a&7)+1); 
} 

unsigned int fun_two (unsigned int *a) 
{ 
    return((*a&7)+1); 
} 

Không tối ưu hóa, bạn có thêm một hướng dẫn khi chuyển qua tham chiếu để tải giá trị tại địa chỉ đó để làm điều gì đó với nó.

 
00000000 : 
    0: e52db004 push {fp}  ; (str fp, [sp, #-4]!) 
    4: e28db000 add fp, sp, #0 
    8: e24dd00c sub sp, sp, #12 
    c: e50b0008 str r0, [fp, #-8] 
    10: e51b3008 ldr r3, [fp, #-8] 
    14: e2033007 and r3, r3, #7 
    18: e2833001 add r3, r3, #1 
    1c: e1a00003 mov r0, r3 
    20: e28bd000 add sp, fp, #0 
    24: e49db004 pop {fp}  ; (ldr fp, [sp], #4) 
    28: e12fff1e bx lr 

0000002c : 
    2c: e52db004 push {fp}  ; (str fp, [sp, #-4]!) 
    30: e28db000 add fp, sp, #0 
    34: e24dd00c sub sp, sp, #12 
    38: e50b0008 str r0, [fp, #-8] 
    3c: e51b3008 ldr r3, [fp, #-8] 
    40: e5933000 ldr r3, [r3] 
    44: e2033007 and r3, r3, #7 
    48: e2833001 add r3, r3, #1 
    4c: e1a00003 mov r0, r3 
    50: e28bd000 add sp, fp, #0 
    54: e49db004 pop {fp}  ; (ldr fp, [sp], #4) 
    58: e12fff1e bx lr 

Tối ưu hóa, -O1 đến -O3 cho kết quả tương tự. Và bạn vẫn bị mất lệnh tải giá trị.

 
00000000 : 
    0: e2000007 and r0, r0, #7 
    4: e2800001 add r0, r0, #1 
    8: e12fff1e bx lr 

0000000c : 
    c: e5900000 ldr r0, [r0] 
    10: e2000007 and r0, r0, #7 
    14: e2800001 add r0, r0, #1 
    18: e12fff1e bx lr 

Và nó sẽ tiếp tục như thế cho khá nhiều bất cứ điều gì có kích thước duy nhất bạn có thể vượt qua trong. 64 integeters chút, bạn vẫn có ghi hướng dẫn và bộ nhớ chu kỳ phụ tải từ tham chiếu vào thanh ghi để hoạt động . Bất kỳ mảng nào của somethings tốt bạn không thể thực sự làm một vượt qua bằng giá trị có thể bạn? Nhưng một cấu trúc bạn có thể, và tiếp cận với một cấu trúc, tham chiếu hay không, sẽ yêu cầu một số địa chỉ có thể.

 
typedef struct 
{ 
    unsigned int a; 
    unsigned int b; 
    char c[4]; 
} ruct; 

unsigned int fun_one (ruct a) 
{ 
    return((a.c[3]&7)+1); 
} 

unsigned int fun_two (ruct *a) 
{ 
    return((a->c[3]&7)+1); 
} 

Không tối ưu hóa, chúng tôi bắt đầu với mỗi câu lệnh 12 câu. Tôi sẽ phải nhìn chằm chằm vào nó nhiều hơn để quyết định nếu một trong những đốt cháy chu kỳ đồng hồ nhiều hơn khác.

 
00000000 : 
    0: e52db004 push {fp}  ; (str fp, [sp, #-4]!) 
    4: e28db000 add fp, sp, #0 
    8: e24dd014 sub sp, sp, #20 
    c: e24b3010 sub r3, fp, #16 
    10: e8830007 stm r3, {r0, r1, r2} 
    14: e55b3005 ldrb r3, [fp, #-5] 
    18: e2033007 and r3, r3, #7 
    1c: e2833001 add r3, r3, #1 
    20: e1a00003 mov r0, r3 
    24: e28bd000 add sp, fp, #0 
    28: e49db004 pop {fp}  ; (ldr fp, [sp], #4) 
    2c: e12fff1e bx lr 

00000030 : 
    30: e52db004 push {fp}  ; (str fp, [sp, #-4]!) 
    34: e28db000 add fp, sp, #0 
    38: e24dd00c sub sp, sp, #12 
    3c: e50b0008 str r0, [fp, #-8] 
    40: e51b3008 ldr r3, [fp, #-8] 
    44: e5d3300b ldrb r3, [r3, #11] 
    48: e2033007 and r3, r3, #7 
    4c: e2833001 add r3, r3, #1 
    50: e1a00003 mov r0, r3 
    54: e28bd000 add sp, fp, #0 
    58: e49db004 pop {fp}  ; (ldr fp, [sp], #4) 
    5c: e12fff1e bx lr 

Nhưng hãy xem điều gì xảy ra với tối ưu hóa. Cấu trúc được kích thước như vậy mà nó phù hợp trong thanh ghi khi được thông qua vào.

 
00000000 : 
    0: e24dd010 sub sp, sp, #16 
    4: e28d3004 add r3, sp, #4 
    8: e8830007 stm r3, {r0, r1, r2} 
    c: e5dd100f ldrb r1, [sp, #15] 
    10: e2010007 and r0, r1, #7 
    14: e2800001 add r0, r0, #1 
    18: e28dd010 add sp, sp, #16 
    1c: e12fff1e bx lr 

00000020 : 
    20: e5d0100b ldrb r1, [r0, #11] 
    24: e2010007 and r0, r1, #7 
    28: e2800001 add r0, r0, #1 
    2c: e12fff1e bx lr 

Đáng buồn thay gcc không làm một công việc rất tốt việc tối ưu hóa thế này, có thể làm một sự thay đổi và và trong một hướng dẫn trên r3, một add, và bx, lr, ba hướng dẫn, đánh bại vượt qua bằng cách tham chiếu.

Bạn cần biết trình biên dịch và giao diện, nó có vượt qua đối số trong sổ đăng ký hay luôn trên ngăn xếp không? Nếu thanh ghi được sử dụng nó sẽ làm gì nếu các đối số của bạn cần nhiều không gian hơn các thanh ghi dự trữ có thể xử lý, nó điền chúng lên sau đó sử dụng ngăn xếp, nó chỉ sử dụng ngăn xếp và không có thanh ghi? Nó có vượt qua một con trỏ đến bộ nhớ giữ đối số, vượt qua theo kiểu tham chiếu, nhưng sao cho giá trị được truyền vào được bảo vệ.

Bạn cũng phải xem xét vượt quá các chức năng riêng lẻ về số lượng bộ nhớ và đăng ký công việc phải xảy ra để chuẩn bị cuộc gọi đến hàm. Vượt qua tham chiếu cho ví dụ cấu trúc sẽ là một tải đơn hoặc ngay lập tức để điền vào một thanh ghi có địa chỉ của cấu trúc. Việc truyền theo giá trị của cấu trúc, trong trường hợp ARM sẽ là một lệnh đơn để nạp ba thanh ghi với cấu trúc, nhưng phải mất ba chu kỳ đồng hồ (hoặc 6 hoặc 2 tùy thuộc vào bus amba/axi). Các bộ vi xử lý khác, nó có thể làm bạn mất ba lệnh và một chu kỳ đồng hồ dữ liệu cho mỗi thanh ghi. Vì vậy, ngay cả khi gcc đã thực hiện một công việc tốt hơn để tối ưu hóa pass bằng ví dụ cấu trúc giá trị, thì việc vượt qua bằng tham chiếu có thể chỉ cắt nó bằng một chu kỳ đồng hồ hoặc hai nhưng điều đó phụ thuộc rất nhiều vào mã trong hàm gọi. Để thực sự biết bạn phải kiểm tra nó bằng cách định thời gian mã một cách chính xác và tháo rời để hiểu tại sao nó lại nhanh hơn hoặc chậm hơn khi bạn điều chỉnh nó.

0

Nếu bạn đang chuyển một giá trị chỉ sử dụng một số cuộc gọi hàm sâu, thì có thể là hiệu quả hơn để chuyển qua tham chiếu đến T-T). Nếu đó là trường hợp, mặc dù, bạn đang lộ chi tiết thực hiện vì lợi ích của "tối ưu hóa" sớm.

tôi nghi ngờ rằng trong đa số trường hợp, bạn sẽ bị mất hiệu suất đáng kể do sự tối ưu hóa trình biên dịch không còn có thể làm (bởi vì bạn có một biến address-thực hiện, và con trỏ đã trốn thoát):

  • Biến không thể sống trong sổ đăng ký.
  • Biến phải sống đến cuối hàm cuối cùng trong phạm vi của nó (nghĩa là biến không thể được sử dụng lại để lưu trữ một biến khác).
  • Biến có thể thay đổi qua các cuộc gọi hàm, có nghĩa là trình biên dịch phải quên mọi thứ có thể đã biết về nó giữa các cuộc gọi (ví dụ: số dương/số không).

Ví dụ (Tôi đang sử dụng cú pháp con trỏ đến làm cho mọi việc rõ ràng hơn, nhưng điều này cũng đúng đối với các tài liệu tham khảo):

long long x=0,y=1; 

for (int i = 0; i < 10; i++) { 
    x = f(&x); 
    g(&x); 

    y = f(&y); 
    g(&y); 
} 

Khá tiêu chuẩn, nhưng f() và g() có thể là gây phiền nhiễu:

long long f(long long * x) { 
    static long long * old; 
    if (old) { *old++; *x += *old; } 
    return ++*x; 
} 

long long g(long long * x) { 
    static long long * old; 
    if (old == x) { abort(); } 
    printf("%lld\n", *x); 
} 

Bạn có thể sửa chữa một số vấn đề bằng cách sử dụng long long const * (do đó các chức năng không thể sửa đổi giá trị, nhưng họ vẫn có thể đọc từ nó ...).

Bạn có thể khắc phục những bằng cách gắn bó cuộc gọi chức năng bên trong một khối và đi qua một tham chiếu đến một bản sao của biến:

{ 
    long long tmp = x; 
    x = f(&tmp); 
} 
Các vấn đề liên quan