2010-03-17 36 views
5

Tôi cho rằng việc thực hiện ngây thơ của một nhà điều hành + cho ma trận (2D ví dụ) trong C++ sẽ là:Điều Hành + cho ma trận trong C++

class Matrix { 

    Matrix operator+ (const Matrix & other) const { 
     Matrix result; 
     // fill result with *this.data plus other.data 
     return result; 
    } 
} 

vì vậy chúng tôi có thể sử dụng nó như

Matrix a; 
Matrix b; 
Matrix c; 

c = a + b; 

Phải không?

Nhưng nếu ma trận lớn thì điều này không hiệu quả vì chúng tôi đang thực hiện một bản sao không cần thiết (kết quả trả về).

Vì vậy, Nếu chúng ta wan't để có hiệu quả chúng ta phải quên đi những cuộc gọi sạch:

c = a + b; 

Phải không?

Bạn sẽ đề xuất/thích điều gì? Cảm ơn.

+1

Ma trận của bạn lớn đến mức nào? Mục tiêu hiệu suất của bạn là gì? Bạn đã thử đo lường hiệu suất hay chi phí của bản sao? –

+0

Nếu các ma trận thậm chí còn lớn hơn một chút, tôi sẽ nghĩ rằng chi phí trả lại ma trận sẽ nhỏ hơn rất nhiều so với thực sự thực hiện bổ sung – randomThought

+5

Tại sao không chuyển thành 'const Matrix & other'? – kennytm

Trả lời

12

Chuẩn C++ cho phép trình biên dịch tách rời bản sao không cần thiết trong trường hợp này (nó được gọi là "tối ưu hóa giá trị trả về có tên", thường được viết tắt là NRVO). Có một "RVO" phù hợp cho thời điểm bạn trả về tạm thời thay vì biến được đặt tên.

Gần như tất cả các trình biên dịch C++ hợp lý gần đây đều thực hiện cả NRVO và RVO, vì vậy nói chung bạn có thể bỏ qua thực tế rằng cấu trúc này sẽ không được đặc biệt hiệu quả.

Chỉnh sửa: Tôi đã, tất nhiên, đang nói về bản sao liên quan đến việc trả lại ma trận mới giữ kết quả của việc bổ sung. Bạn có thể làm muốn hoặc thông qua các đầu vào bằng cách tham chiếu const:

Matrix operator+(Matrix const &other) const { 
    Matrix result; 
    // ... 
    return result; 
} 

... nếu không, đi qua giá trị, nhưng trả về giá trị thông qua:

Matrix operator+(Matrix other) const { 
    other += *this; 
    return other; 
} 

Lưu ý rằng điều này phụ thuộc vào mặc dù, nó thực sự làm b+a thay vì a+b) vì vậy trong khi nó tốt để bổ sung, nó sẽ không hoạt động đối với một số hoạt động khác.

+0

Dave Abrahams có một blog tuyệt vời về phương pháp tiếp cận theo giá trị tại đây: http://cpp-next.com/archive/2009/08/want-speed-pass -by-value/ – Void

+0

Chẳng phải ví dụ mã thứ hai được gọi là toán tử + =? Điều đó phù hợp với những gì thực hiện thực hiện và sau đó nó có thể trả về một tham chiếu, chứ không phải giá trị. –

+0

@Laurynas: Tại sao ví dụ mã 2 được gọi là 'toán tử + ='? 'this' không được sửa đổi ở đó. – kennytm

3

Bạn có thể trả lại giá trị mà không kích hoạt quá trình xây dựng bản sao. Tài liệu tham khảo R-Value của nó được gọi là Giải thích chi tiết tại đây http://www.artima.com/cppsource/rvalue.html

+5

tham chiếu rvalue là một tính năng mới cho C++ 0x, vì vậy không phải tất cả các trình biên dịch hiện tại đều bao gồm chúng. Chúng cũng không cần thiết trong trường hợp này. –

0

Hai cách để giải quyết.

1) Sử dụng tài liệu tham khảo:

Matrix& operator+ (Matrix& other) const { 

2) Sử dụng bản sao cạn trong constructor sao chép. Vâng, đối tượng Matrix mới sẽ được tạo ra nhưng các ma trận thực sẽ không được tạo ra một lần nữa

+0

Phiên bản đầu tiên của bạn "Matrix & operator + (Matrix & other)", tôi tin rằng, sửa đổi toán tử bên trái của "+". Vì vậy, nếu bạn viết "a = b + c;", b sẽ được sửa đổi. Vì vậy, bạn nên giới hạn mình thành "b + c;" để có kết quả chính xác ... – paercebal

+0

hmmm. nó sẽ sửa đổi b như thế nào? nó sẽ gọi phương thức operator + cho b, chuyển tham chiếu của tham chiếu c và trả về. – Andrey

+0

Tóm lại: trong trường hợp này, việc trả lại tham chiếu sẽ không mua cho bạn bất kỳ thứ gì. Tổng của hai đầu vào sẽ (bình thường) khác với hai đầu vào đó. theo cách này hay cách khác, bạn cần tạo một ma trận mới để giữ kết quả đó. –

0

tôi sẽ cố gắng để xây dựng Matrix tại câu lệnh return:

Matrix Matrix::operator + (const Matrix& M) 
{ 
    return Matrix(
     // 16 parameters defining the 4x4 matrix here 
     // e.g. m00 + M.m00, m01 + M.m01, ... 
    ); 
} 

Bằng cách đó bạn sẽ không được sử dụng bất kỳ temporaries .

+0

Nói chung không khả thi cho các thứ nguyên tùy ý. Ngoài ra, nó có thể dẫn đến hiệu suất kém, vì các kết quả ma trận lần đầu tiên được đẩy lên ngăn xếp cuộc gọi (các hàm thường không thể đặt 16 tham số trong thanh ghi khi tối ưu hóa ABI gọi của chúng), sau đó hàm tạo sao chép chúng vào đối tượng. –

+0

Nếu đây là lớp ma trận 4x4 cố định, thì nó hoạt động vì hầu hết các trình biên dịch có thể áp dụng tối ưu hóa giá trị trả về (RVO) để bỏ ẩn tạm thời. Nếu đó là một lớp ma trận chung với số hàng và cột tùy ý, bạn sẽ phải tạo tạm thời. Như một câu trả lời trước đó cho thấy, tạm thời đó cũng có thể được tối ưu hóa bằng cách tối ưu hóa giá trị trả về có tên (NRVO). –

1

Như những người khác đã chỉ ra, việc triển khai của bạn không đắt như bạn nghĩ. Tuy nhiên, nó có thể làm cho một số ý nghĩa để xác định các phương pháp bổ sung mà sửa đổi các đối tượng tại chỗ để sử dụng trong vòng bên trong quan trọng.

EDIT - cố định sau đoạn

Vấn đề ở đây là ngay cả với optimisations giá trị trả về, bạn vẫn kết thúc xây dựng một biến địa phương và sau đó gán đó để biến kết quả sau khi điều hành + thoát. Và hủy bỏ vật thể phụ đó, tất nhiên rồi. Vẫn còn một đối tượng phụ được sử dụng để tạm giữ kết quả. Có thể thực hiện việc đếm tham chiếu bằng tính năng copy-on-write, nhưng điều đó làm tăng thêm chi phí hội nghị cho mỗi lần sử dụng ma trận.

Đây không phải là vấn đề đối với 99% trường hợp, nhưng thỉnh thoảng bạn gặp phải trường hợp quan trọng. Nếu bạn đang xử lý các ma trận lớn, chi phí đếm tham chiếu sẽ không đáng kể - nhưng đối với 2D lên đến 4D có những lúc bạn có thể quan tâm nhiều đến vài chu kỳ bổ sung đó - hoặc nhiều hơn về điểm, về không đặt ma trận trên heap khi bạn muốn nó trên stack hoặc nhúng trong một số struct/class/array.

Điều đó nói rằng - trong những trường hợp đó, có thể bạn sẽ không viết mã ma trận của riêng mình - bạn sẽ chỉ sử dụng ma trận ops từ DirectX hoặc OpenGL hoặc bất kỳ thứ gì.

+0

@ Steve314: toàn bộ điểm của [N] RVO là bạn * không * sao chép xây dựng giá trị trả lại. Thay vào đó, nó chỉ đơn giản là sử dụng nơi trả về sẽ đi * như * đối tượng "cục bộ". –

+0

@ Jerry - Tôi nên nói "chuyển nhượng". Trong dòng "c = a + b", bạn sẽ nhận được bài tập "c = ". Các là riêng biệt từ a, b và c, và là một chi phí. Chi phí trên được đảm bảo xảy ra vì nếu không đánh giá biểu thức sẽ không liên quan đến cả toán tử + và toán tử =. Bạn cần một tạm thời ở đó để cung cấp cho một cái gì đó chuyển nhượng (mà không phải là một cũng không b) để sao chép từ. [N] RVO tối ưu hóa việc sao chép từ bên trong toán tử + sang bên ngoài, nhưng một đối tượng trung gian vẫn còn đó và một op thay đổi tại chỗ thay cho c sẽ tránh được chi phí đó. – Steve314

+0

@Jerry - đó là "nhà xây dựng bản sao" mà tôi đã nói đến là vì tôi bằng cách nào đó đang nghĩ "Ma trận c (a + b);" thay vì "c = a + b;". Trình tạo bản sao đó không liên quan gì đến cách toán tử + được triển khai hoặc được tối ưu hóa - nó chỉ đơn giản sử dụng kết quả (đối tượng tạm thời). – Steve314

0
class Matrix { 

    Matrix & operator+=(const Matrix & other) { 
     // fill result with *this.data plus other.data 
     // elements += other elements 
     return *this; 
    } 
    Matrix operator+ (const Matrix & other) { 
     Matrix result = *this; 
     return result += other; 
    } 
} 
1

Nếu bạn thực sự lo lắng về hiệu suất (bạn đã lược tả?), Bạn có lẽ không nên triển khai toán tử + vì bạn không thể kiểm soát nếu nó sẽ tạo ra tạm thời không tối ưu. Chỉ cần thực hiện toán tử + = và/hoặc hàm thành viên add(Matrix& result, const Matrix& in1, const Matrix& in2) và để cho khách hàng của bạn tạo ra các thời gian chính xác.

Nếu bạn muốn điều hành + Jerry Coffin sẽ hoạt động tốt.

2

Lưu ý rằng triển khai ngây thơ đầu tiên của bạn rất bản địa, vì không có gì được chuyển qua tham chiếu. Tôi cho rằng đây là một ví dụ thực sự ngây thơ và không cần phải nhắc nhở độc giả về những lợi ích của việc chuyển qua tham chiếu thay vì bằng giá trị.

Cũng lưu ý rằng tôi đã sử dụng toán tử không phải thành viên, thay vì hàm thành viên, nhưng cuối cùng, kết quả là (gần như) giống nhau.

Nếu bạn muốn chắc chắn không có bản sao cần thiết sẽ được tạo ra, bạn nên thử một phiên bản không điều hành:

void add(Matrix & result, const Matrix & lhs, const Matrix & rhs) ; 

Nếu bạn muốn làm điều đó một cách điều hành (đó là giải pháp ưa thích của tôi) , thì bạn nên giả định toán tử + sẽ tạo tạm thời. Sau đó bạn cần xác định cả hai toán tử + và operator + =:

Matrix & operator += (Matrix & result, const Matrix & rhs) ; 
{ 
    // add rhs to result, and return result 
    return result ; 
} 

Matrix operator + (const Matrix & lhs, const Matrix & rhs) ; 
{ 
    Matrix result(lhs) ; 
    result += rhs ; 
    return result ; 
} 

Bây giờ, bạn có thể thử "đòn bẩy" optimisations trình biên dịch và viết nó như:

Matrix & operator += (Matrix & result, const Matrix & rhs) ; 
{ 
    // add rhs to result, and return result 
    return result ; 
} 

Matrix operator + (Matrix lhs, const Matrix & rhs) 
{ 
    return lhs += rhs ; 
} 

Theo đề xuất của Herb Sutter trong C++ coding tiêu chuẩn, 27. Ưu tiên các hình thức kinh điển của số học và phân công khai thác, p48-49:

Một biến là có toán tử @ [@ being +, -, bất cứ điều gì] chấp nhận thông số đầu tiên của nó theo giá trị. Bằng cách này, bạn sắp xếp cho trình biên dịch để thực hiện sao chép cho bạn một cách ngầm định, và điều này có thể làm cho trình biên dịch mất nhiều thời gian hơn trong việc áp dụng tối ưu hóa.

+0

Tôi không thể tin rằng mình đã sử dụng từ khóa "đòn bẩy". Tôi có thể downmod bản thân mình cho rằng ... :-D ... – paercebal

+0

Một ma trận 2x2 là khá nhỏ.Chi phí sao chép nó vào các tham số có thể nhỏ hơn chi phí từ cuộc gọi tham chiếu. Giả sử, tất nhiên, 2D có nghĩa là 2 * 2 - theo kinh nghiệm của tôi, ma trận luôn là 2D nhưng "2D" được sử dụng rất nhiều để có nghĩa là 2 * 2, "3D" có nghĩa là 3 * 3 v.v. – Steve314

+0

@ Steve314: Khi tôi làm việc trên vật lý, hoặc xử lý hình ảnh, ma trận chúng tôi sử dụng là ma trận 4x4 2D với giá trị thực, có nghĩa là ngay cả với mảng tối ưu gấp đôi [16], chi phí sao chép đối tượng lớn hơn chi phí sao chép địa chỉ của nó . Ngay cả với ma trận "ngắn" 2x2 2D, chúng tôi sẽ có một [4] ngắn, là 64 bit, một lần nữa, hoặc lớn hơn một địa chỉ trên 32-bit, hoặc hoàn toàn bằng một địa chỉ trên 64-bit ... [ĐỂ ĐƯỢC TIẾP TỤC] – paercebal