2012-01-17 19 views
26

Tôi đang tối ưu hóa một hàm tạo được gọi trong một trong các vòng trong cùng của ứng dụng của chúng tôi. Lớp trong câu hỏi rộng khoảng 100 byte, bao gồm một bó của int s, float s, bool s và các cấu trúc tầm thường, và nên được sao chép một cách trivially (nó có một hàm tạo mặc định không độc hại, nhưng không có hàm hủy hoặc hàm ảo). Nó được xây dựng thường đủ để mỗi nano giây thời gian sử dụng trong ctor này hoạt động với khoảng 6.000 đô la phần cứng máy chủ bổ sung mà chúng tôi cần mua.GCC có thể bị ép buộc để tạo ra các nhà thầu hiệu quả cho các đối tượng liên kết bộ nhớ không?

Tuy nhiên, tôi thấy rằng GCC không phát ra mã rất hiệu quả cho hàm tạo này (ngay cả với -O3 -march v.v.). Việc thực thi hàm tạo của GCC, điền vào các giá trị mặc định thông qua một danh sách khởi tạo, mất khoảng 34ns để chạy. Nếu thay vì hàm tạo mặc định này, tôi sử dụng hàm viết tay ghi trực tiếp vào không gian bộ nhớ của đối tượng với một loạt các nội tại và toán tử con trỏ của SIMD, việc xây dựng mất khoảng 8ns.

Tôi có thể yêu cầu GCC phát ra một hàm tạo hiệu quả cho các đối tượng như vậy khi tôi __attribute__ chúng được liên kết với bộ nhớ trên ranh giới SIMD không? Hay tôi phải sử dụng kỹ thuật cũ của trường như viết bộ khởi tạo bộ nhớ của riêng mình trong hội đồng?

Đối tượng này chỉ được xây dựng dưới dạng cục bộ trên ngăn xếp, vì vậy, mọi chi phí mới/malloc đều không áp dụng.

Bối cảnh:

Lớp này được sử dụng bằng cách xây dựng nó trên stack như là một biến địa phương, lựa chọn cách viết một vài lĩnh vực có giá trị không phải mặc định, và sau đó đi qua nó (bằng cách tham khảo) để một chức năng, mà chuyển tham chiếu của nó đến một cái khác và vân vân.

struct Trivial { 
    float x,y,z; 
    Trivial() : x(0), y(0), z(0) {}; 
}; 

struct Frobozz 
{ 
    int na,nb,nc,nd; 
    bool ba,bb,bc; 
    char ca,cb,cc; 
    float fa,fb; 
    Trivial va, vb; // in the real class there's several different kinds of these 
    // and so on 
    Frobozz() : na(0), nb(1), nc(-1), nd(0), 
       ba(false), bb(true), bc(false), 
       ca('a'), cb('b'), cc('c'), 
       fa(-1), fb(1.0) // etc 
    {} 
} __attribute__((aligned(16))); 

// a pointer to a func that takes the struct by reference 
typedef int (*FrobozzSink_t)(Frobozz&); 

// example of how a function might construct one of the param objects and send it 
// to a sink. Imagine this is one of thousands of event sources: 
int OversimplifiedExample(int a, float b) 
{ 
    Frobozz params; 
    params.na = a; params.fb = b; // other fields use their default values 
    FrobozzSink_t funcptr = AssumeAConstantTimeOperationHere(); 
    return (*funcptr)(params); 
} 

Nhà xây dựng tối ưu ở đây sẽ hoạt động bằng cách sao chép từ cá thể "mẫu" tĩnh vào cá thể mới được xây dựng, lý tưởng sử dụng toán tử SIMD để làm việc 16 byte tại một thời điểm. Thay vào đó GCC thực hiện chính xác điều sai đối với OversimplifiedExample() — một loạt các ops ngay lập tức để điền vào cấu trúc byte-by-byte.

// from objdump -dS 
int OversimplifiedExample(int a, float b) 
{ 
    a42:55     push %ebp 
    a43:89 e5    mov %esp,%ebp 
    a45:53     push %ebx 
    a46:e8 00 00 00 00  call a4b <_Z21OversimplifiedExampleif+0xb> 
    a4b:5b     pop %ebx 
    a4c:81 c3 03 00 00 00 add $0x3,%ebx 
    a52:83 ec 54    sub $0x54,%esp 
    // calling the 'Trivial()' constructors which move zero, word by word... 
    a55:89 45 e0    mov %eax,-0x20(%ebp) 
    a58:89 45 e4    mov %eax,-0x1c(%ebp) 
    a5b:89 45 e8    mov %eax,-0x18(%ebp) 
    a5e:89 45 ec    mov %eax,-0x14(%ebp) 
    a61:89 45 f0    mov %eax,-0x10(%ebp) 
    a64:89 45 f4    mov %eax,-0xc(%ebp) 
    // filling out na/nb/nc/nd.. 
    a67:c7 45 c4 01 00 00 00 movl $0x1,-0x3c(%ebp) 
    a71:c7 45 c8 ff ff ff ff movl $0xffffffff,-0x38(%ebp) 
    a78:89 45 c0    mov %eax,-0x40(%ebp) 
    a7b:c7 45 cc 00 00 00 00 movl $0x0,-0x34(%ebp) 
    a82:8b 45 0c    mov 0xc(%ebp),%eax 
    // doing the bools and chars by moving one immediate byte at a time! 
    a85:c6 45 d0 00   movb $0x0,-0x30(%ebp) 
    a89:c6 45 d1 01   movb $0x1,-0x2f(%ebp) 
    a8d:c6 45 d2 00   movb $0x0,-0x2e(%ebp) 
    a91:c6 45 d3 61   movb $0x61,-0x2d(%ebp) 
    a95:c6 45 d4 62   movb $0x62,-0x2c(%ebp) 
    a99:c6 45 d5 63   movb $0x63,-0x2b(%ebp) 
    // now the floats... 
    a9d:c7 45 d8 00 00 80 bf movl $0xbf800000,-0x28(%ebp) 
    aa4:89 45 dc    mov %eax,-0x24(%ebp) 
    // FrobozzSink_t funcptr = GetFrobozz(); 
    aa7:e8 fc ff ff ff  call aa8 <_Z21OversimplifiedExampleif+0x68> 
    // return (*funcptr)(params); 
    aac:8d 55 c0    lea -0x40(%ebp),%edx 
    aaf:89 14 24    mov %edx,(%esp) 
    ab2:ff d0    call *%eax 
    ab4:83 c4 54    add $0x54,%esp 
    ab7:5b     pop %ebx 
    ab8:c9     leave 
    ab9:c3     ret 
} 

Tôi cố gắng để khuyến khích GCC để xây dựng một đơn 'mẫu mặc định' của đối tượng này, và sau đó số lượng lớn-sao chép nó trong constructor mặc định, bằng cách làm một chút thủ đoạn gian trá với một 'giả' constructor ẩn mà làm các cơ sở mẫu mực và sau đó có mặc định chỉ cần sao chép nó:

struct Frobozz 
{ 
    int na,nb,nc,nd; 
    bool ba,bb,bc; 
    char ca,cb,cc; 
    float fa,fb; 
    Trivial va, vb; 
    inline Frobozz(); 
private: 
    // and so on 
    inline Frobozz(int dummy) : na(0), /* etc etc */  {} 
} __attribute__((aligned(16))); 

Frobozz::Frobozz() 
{ 
    const static Frobozz DefaultExemplar(69105); 
    // analogous to copy-on-write idiom 
    *this = DefaultExemplar; 
    // or: 
    // memcpy(this, &DefaultExemplar, sizeof(Frobozz)); 
} 

Nhưng điều này tạo ra ngay cả chậm mã hơn mặc định cơ bản với danh sách initializer, do một số đống sao chép dự phòng.

Cuối cùng tôi đành phải viết một chức năng inlined tự do làm những bước *this = DefaultExemplar, sử dụng intrinsics trình biên dịch và giả định về sự liên kết bộ nhớ để cấp pipelinedMOVDQA opcodes SSE2 đó sao chép các struct hiệu quả. Điều này đã cho tôi hiệu suất tôi cần, nhưng nó rất khốc liệt. Tôi nghĩ rằng ngày của tôi viết initializers trong lắp ráp được phía sau tôi, và tôi thực sự muốn chỉ có tối ưu hóa của GCC phát ra mã ngay tại địa điểm đầu tiên.

Có cách nào tôi có thể nhận GCC để tạo mã tối ưu cho hàm dựng của tôi, một số cài đặt trình biên dịch hoặc bổ sung __attribute__ Tôi đã bỏ lỡ?

Đây là GCC 4.4 chạy trên Ubuntu.Cờ biên dịch bao gồm -m32 -march=core2 -O3 -fno-strict-aliasing -fPIC (trong số những người khác). Khả năng di động là không phải là xem xét và tôi hoàn toàn sẵn sàng hy sinh tuân thủ các tiêu chuẩn về hiệu suất tại đây.

Thời gian được thực hiện bằng cách trực tiếp đọc các tem thời gian truy cập với rdtsc, ví dụ đo một vòng lặp của N OversimplifiedExample() gọi giữa các mẫu với quan tâm đúng mức đến độ phân giải hẹn giờ và bộ nhớ cache và ý nghĩa thống kê và vân vân.

Tôi cũng đã tối ưu hóa điều này bằng cách giảm số lượng trang web gọi càng nhiều càng tốt, tất nhiên, nhưng tôi vẫn muốn biết làm thế nào để nói chung có được ctors tốt hơn trong GCC.

+0

Bạn đã thử GCC mới hơn, như 4.6.2 (hoặc bản chụp mới nhất của bản phát hành sớm sẽ được phát hành 4.7)? –

+1

Bạn có thể bỏ qua định nghĩa của hàm tạo và viết nó hoàn toàn bằng tay trong asm không? Rủi ro và khó khăn để duy trì, nhưng đối với 34 * $ 6000 nó sẽ trả cho chính nó Tôi nghi ngờ – Flexo

+1

Bạn cũng đã thử thêm một số cờ '-msse' khác nhau chưa? Tôi nghĩ rằng họ cần thiết cho sse trong một số trường hợp. Ngoài ra tôi đề nghị bạn chỉ cần có được một gcc gần đây và duyệt manpage của nó, suy nghĩ về nếu mọi tùy chọn có thể cải thiện tình hình của bạn và sau đó thử nó ra. – PlasmaHH

Trả lời

8

Đây là cách tôi sẽ thực hiện. Không khai báo bất kỳ hàm tạo nào; thay vào đó, khai báo một frobozz cố định có chứa các giá trị mặc định:

const Frobozz DefaultFrobozz = 
    { 
    0, 1, -1, 0,  // int na,nb,nc,nd; 
    false, true, false, // bool ba,bb,bc; 
    'a', 'b', 'c',  // char ca,cb,cc; 
    -1, 1.0    // float fa,fb; 
    } ; 

Sau đó, trong OversimplifiedExample:

Frobozz params (DefaultFrobozz) ; 

Với gcc -O3 (phiên bản 4.5.2), initialisation của params suy biến thành

leal -72(%ebp), %edi 
movl $_DefaultFrobozz, %esi 
movl $16, %ecx 
rep movsl 

gần như tốt như trong môi trường 32 bit.

Cảnh báo: Tôi đã thử với phiên bản 64 bit g ++ 4.7.0 20110827 (thử nghiệm), và nó tạo ra một chuỗi rõ ràng các bản sao 64 bit thay vì di chuyển khối. Bộ xử lý không cho phép rep movsq, nhưng tôi hy vọng rep movsl sẽ nhanh hơn một chuỗi tải và cửa hàng 64 bit. Có lẽ không. (Nhưng công tắc -Os - tối ưu hóa cho không gian - sử dụng hướng dẫn rep movsl.) Dù sao, hãy thử điều này và cho chúng tôi biết điều gì sẽ xảy ra.

Đã chỉnh sửa để thêm: Tôi đã sai về bộ xử lý không cho phép rep movsq. Tài liệu của Intel cho biết "Các hướng dẫn MOVS, MOVSB, MOVSW và MOVSD có thể được tiền tố REP đi trước", nhưng có vẻ như đây chỉ là một trục trặc tài liệu. Trong mọi trường hợp, nếu tôi làm cho Frobozz đủ lớn, thì trình biên dịch 64 bit sẽ tạo ra các hướng dẫn rep movsq; vì vậy nó có thể biết những gì nó đang làm.

+0

"Không khai báo bất kỳ hàm tạo nào" - bạn có thể khai báo-private-without-define (hoặc xóa) hàm tạo no-arg, để đảm bảo rằng không ai vô tình kết thúc với một đối tượng chưa được khởi tạo. Họ hoặc là sao chép mặc định, hoặc họ sử dụng một danh sách initializer, nhưng họ không thể chỉ viết 'Frobozz params;'. Cá nhân tôi cảm thấy hạnh phúc hơn về mã hiện tại nếu hàm tạo mặc định biến mất hoàn toàn, thay vì thay đổi hành vi của nó để làm điều gì đó sai ;-) –

+3

"nhưng tôi mong đợi rep movsd sẽ nhanh hơn chuỗi tải 64 bit và cửa hàng "có ngưỡng nơi lệnh' REP MOVS' thường sẽ chậm hơn. Ngoài ra, 'REP MOVS' yêu cầu 3 thanh ghi rõ ràng' ECX', 'ESI' và' EDI', điều này có thể dẫn đến các thanh ghi quá mức xáo trộn/tràn ra như bị chặn để chặn các bản sao. – Necrolis

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