Bộ thu giá trị so với bộ thu con trỏ


107

Tôi không rõ trong trường hợp nào thì tôi muốn sử dụng bộ thu giá trị thay vì luôn sử dụng bộ thu con trỏ.
Để tóm tắt từ tài liệu:

type T struct {
    a int
}
func (tv  T) Mv(a int) int         { return 0 }  // value receiver
func (tp *T) Mp(f float32) float32 { return 1 }  // pointer receiver

Các tài liệu cũng cho biết "Đối với các kiểu như kiểu cơ bản, các lát cắt và cấu trúc nhỏ, bộ thu giá trị rất rẻ, vì vậy trừ khi ngữ nghĩa của phương thức yêu cầu một con trỏ, bộ nhận giá trị hiệu quả và rõ ràng."

Điểm đầu tiên nó nói nó là "rất rẻ", nhưng câu hỏi nhiều hơn là nó rẻ hơn sau đó là máy thu con trỏ. Vì vậy, tôi đã thực hiện một điểm chuẩn nhỏ (mã trên ý chính) cho tôi thấy rằng bộ thu con trỏ nhanh hơn ngay cả đối với một cấu trúc chỉ có một trường chuỗi. Đây là những kết quả:

// Struct one empty string property
BenchmarkChangePointerReceiver  2000000000               0.36 ns/op
BenchmarkChangeItValueReceiver  500000000                3.62 ns/op


// Struct one zero int property
BenchmarkChangePointerReceiver  2000000000               0.36 ns/op
BenchmarkChangeItValueReceiver  2000000000               0.36 ns/op

(Chỉnh sửa: Xin lưu ý rằng điểm thứ hai trở nên không hợp lệ trong các phiên bản go mới hơn, xem nhận xét) .
Điểm thứ hai mà nó nói, đó là "hiệu quả và rõ ràng", đó là vấn đề về hương vị, phải không? Cá nhân tôi thích nhất quán bằng cách sử dụng ở mọi nơi theo cùng một cách. Hiệu quả theo nghĩa nào? hiệu suất khôn ngoan có vẻ như con trỏ hầu như luôn luôn hiệu quả hơn. Một vài lần chạy thử nghiệm với một thuộc tính int cho thấy lợi thế tối thiểu của bộ thu Giá trị (phạm vi 0,01-0,1 ns / op)

Ai đó có thể cho tôi biết một trường hợp mà người nhận giá trị rõ ràng có ý nghĩa hơn sau đó là người nhận con trỏ không? Hay tôi đang làm sai điểm chuẩn, tôi đã bỏ qua các yếu tố khác?


3
Tôi đã chạy các điểm chuẩn tương tự với một trường chuỗi đơn và cũng với hai trường: trường chuỗi và trường int. Tôi nhận được kết quả nhanh hơn từ bộ thu giá trị. BenchmarkChangePointerReceiver-4 10000000000 0,99 ns / op BenchmarkChangeItValueReceiver-4 10000000000 0,33 ns / op Đây là sử dụng Go 1.8. Tôi tự hỏi liệu có tối ưu hóa trình biên dịch được thực hiện kể từ lần cuối bạn chạy các điểm chuẩn hay không. Xem ý chính để biết thêm chi tiết.
pbitty

2
Bạn đúng. Chạy điểm chuẩn ban đầu của tôi bằng Go1.9, bây giờ tôi cũng nhận được các kết quả khác nhau. Bộ thu con trỏ 0,60 ns / op, Bộ nhận giá trị 0,38 ns / op
Chrisport 27/1117

Câu trả lời:


117

Lưu ý rằng Câu hỏi thường gặp đề cập đến tính nhất quán

Tiếp theo là tính nhất quán. Nếu một số phương thức của kiểu phải có bộ thu con trỏ, thì phần còn lại cũng vậy, vì vậy bộ phương pháp nhất quán bất kể kiểu được sử dụng như thế nào. Xem phần về bộ phương pháp để biết chi tiết.

Như đã đề cập trong chủ đề này :

Quy tắc về con trỏ so với giá trị cho bộ nhận là các phương thức giá trị có thể được gọi trên con trỏ và giá trị, nhưng phương thức con trỏ chỉ có thể được gọi trên con trỏ

Hiện nay:

Ai đó có thể cho tôi biết một trường hợp mà người nhận giá trị rõ ràng có ý nghĩa hơn sau đó là người nhận con trỏ không?

Các bình luận Mã xét có thể giúp:

  • Nếu người nhận là bản đồ, func hoặc chan, không sử dụng con trỏ vào nó.
  • Nếu bộ nhận là một lát cắt và phương thức không liên kết lại hoặc phân bổ lại lát cắt, đừng sử dụng con trỏ đến nó.
  • Nếu phương thức cần thay đổi bộ thu, bộ thu phải là một con trỏ.
  • Nếu người nhận là một cấu trúc chứa sync.Mutex trường đồng bộ hóa hoặc tương tự, máy thu phải là một con trỏ để tránh sao chép.
  • Nếu bộ thu là một cấu trúc hoặc mảng lớn, bộ thu con trỏ sẽ hiệu quả hơn. Lớn như thế nào là lớn? Giả sử nó tương đương với việc chuyển tất cả các phần tử của nó làm đối số cho phương thức. Nếu điều đó cảm thấy quá lớn, nó cũng quá lớn đối với người nhận.
  • Hàm hoặc các phương thức, đồng thời hoặc khi được gọi từ phương thức này, có thể thay đổi bộ thu không? Một kiểu giá trị tạo ra một bản sao của bộ thu khi phương thức được gọi, vì vậy các cập nhật bên ngoài sẽ không được áp dụng cho bộ thu này. Nếu các thay đổi phải hiển thị trong bộ thu ban đầu, bộ thu phải là một con trỏ.
  • Nếu bộ thu là một cấu trúc, mảng hoặc lát cắt và bất kỳ phần tử nào của nó là một con trỏ đến một thứ có thể đang thay đổi, hãy thích một bộ thu con trỏ, vì nó sẽ làm cho người đọc hiểu rõ ràng hơn.
  • Nếu bộ nhận là một mảng nhỏ hoặc cấu trúc tự nhiên là một kiểu giá trị (ví dụ: một cái gì đó giống như time.Timekiểu), không có trường có thể thay đổi và không có con trỏ hoặc chỉ là một kiểu cơ bản đơn giản như int hoặc chuỗi, bộ nhận giá trị sẽ giác quan .
    Bộ nhận giá trị có thể giảm lượng rác có thể được tạo ra; nếu một giá trị được chuyển cho một phương thức giá trị, một bản sao trên ngăn xếp có thể được sử dụng thay vì phân bổ trên heap. (Trình biên dịch cố gắng thông minh trong việc tránh phân bổ này, nhưng nó không phải lúc nào cũng thành công.) Đừng chọn loại nhận giá trị vì lý do này mà không lập hồ sơ trước.
  • Cuối cùng, khi nghi ngờ, hãy sử dụng bộ thu con trỏ.

Phần in đậm được tìm thấy trong net/http/server.go#Write():

// Write writes the headers described in h to w.
//
// This method has a value receiver, despite the somewhat large size
// of h, because it prevents an allocation. The escape analysis isn't
// smart enough to realize this function doesn't mutate h.
func (h extraHeader) Write(w *bufio.Writer) {
...
}

16
The rule about pointers vs. values for receivers is that value methods can be invoked on pointers and values, but pointer methods can only be invoked on pointers Không đúng, thực sự. Cả hai phương thức nhận giá trị và phương thức nhận con trỏ đều có thể được gọi trên một con trỏ hoặc không phải con trỏ được định kiểu chính xác. Bất kể phương thức được gọi là gì, bên trong phần thân phương thức, mã định danh của bộ nhận tham chiếu đến giá trị từng bản sao khi bộ thu giá trị được sử dụng và một con trỏ khi bộ thu con trỏ được sử dụng: Xem play.golang.org/p / 3WHGaAbURM
Hart Simha

3
Có một lời giải thích tuyệt vời ở đây "Nếu x là địa chỉ và & phương pháp thiết lập x chứa m, xm () là viết tắt cho (& x) .m ()."
tera

@tera Có: được thảo luận tại stackoverflow.com/q/43953187/6309
VonC

4
Câu trả lời tuyệt vời nhưng tôi hoàn toàn không đồng ý với điểm này: "vì nó sẽ làm cho ý định rõ ràng hơn", NOPE, một API sạch, X làm đối số và Y làm giá trị trả về là một ý định rõ ràng. Vượt qua một Cấu trúc theo con trỏ và dành thời gian đọc kỹ mã để kiểm tra xem tất cả các thuộc tính được sửa đổi những gì là không rõ ràng, có thể bảo trì được.
Lukas Lukac

@HartSimha Tôi nghĩ rằng bài đăng ở trên chỉ ra thực tế là các phương thức bộ nhận con trỏ không nằm trong "bộ phương thức" cho các loại giá trị. Trong sân chơi liên kết của bạn, thêm dòng sau đây sẽ gây ra lỗi biên dịch: Int(5).increment_by_one_ptr(). Tương tự, một đặc điểm xác định phương thức increment_by_one_ptrsẽ không được thỏa mãn với một giá trị của kiểu Int.
Gaurav Agarwal

16

Để bổ sung thêm cho @VonC câu trả lời tuyệt vời, nhiều thông tin.

Tôi ngạc nhiên là không ai thực sự đề cập đến chi phí bảo trì một khi dự án lớn hơn, các nhà phát triển cũ rời đi và một nhà phát triển mới đến. Go chắc chắn là một ngôn ngữ trẻ.

Nói chung, tôi cố gắng tránh những kẻ chỉ điểm khi tôi có thể nhưng chúng có vị trí và vẻ đẹp của chúng.

Tôi sử dụng con trỏ khi:

  • làm việc với bộ dữ liệu lớn
  • có trạng thái duy trì cấu trúc, ví dụ như TokenCache,
    • Tôi đảm bảo TẤT CẢ các trường đều là RIÊNG TƯ, chỉ có thể tương tác thông qua bộ thu phương thức xác định
    • Tôi không chuyển chức năng này cho bất kỳ quy trình nào

Ví dụ:

type TokenCache struct {
    cache map[string]map[string]bool
}

func (c *TokenCache) Add(contract string, token string, authorized bool) {
    tokens := c.cache[contract]
    if tokens == nil {
        tokens = make(map[string]bool)
    }

    tokens[token] = authorized
    c.cache[contract] = tokens
}

Lý do tại sao tôi tránh con trỏ:

  • con trỏ không an toàn đồng thời (toàn bộ điểm của GoLang)
  • một khi bộ nhận con trỏ, luôn luôn bộ nhận con trỏ (cho tất cả các phương thức của Struct để có tính nhất quán)
  • mutexes chắc chắn đắt hơn, chậm hơn và khó duy trì hơn so với "chi phí sao chép giá trị"
  • nói về "giá trị bản sao chi phí", đó thực sự là một vấn đề? Tối ưu hóa sớm là gốc rễ của tất cả các điều xấu, bạn luôn có thể thêm con trỏ sau
  • nó trực tiếp buộc tôi phải thiết kế các Cấu trúc nhỏ
  • Hầu hết có thể tránh được con trỏ bằng cách thiết kế các chức năng thuần túy với mục đích rõ ràng và I / O rõ ràng
  • tôi tin là thu gom rác khó hơn
  • dễ tranh luận hơn về tính đóng gói, trách nhiệm
  • giữ cho nó đơn giản, ngu ngốc (vâng, con trỏ có thể phức tạp vì bạn không bao giờ biết nhà phát triển của dự án tiếp theo)
  • kiểm tra đơn vị giống như đi bộ qua vườn hồng (biểu thức chỉ tiếng Slovak?), có nghĩa là dễ dàng
  • không có NIL nếu điều kiện (NIL có thể được chuyển ở vị trí mong đợi một con trỏ)

Nguyên tắc chung của tôi, hãy viết càng nhiều phương thức đóng gói càng tốt, chẳng hạn như:

package rsa

// EncryptPKCS1v15 encrypts the given message with RSA and the padding scheme from PKCS#1 v1.5.
func EncryptPKCS1v15(rand io.Reader, pub *PublicKey, msg []byte) ([]byte, error) {
    return []byte("secret text"), nil
}

cipherText, err := rsa.EncryptPKCS1v15(rand, pub, keyBlock) 

CẬP NHẬT:

Câu hỏi này đã thôi thúc tôi nghiên cứu chủ đề nhiều hơn và viết một bài đăng trên blog về chủ đề này https://medium.com/gophersland/gopher-vs-object-oriented-golang-4fa62b88c701


Tôi thích 99% những gì bạn nói ở đây và hoàn toàn đồng ý với nó. Điều đó nói rằng tôi đang tự hỏi liệu ví dụ của bạn có phải là cách tốt nhất để minh họa quan điểm của bạn hay không. Về cơ bản, TokenCache không phải là một bản đồ (từ @VonC - "nếu người nhận là một bản đồ, func hoặc chan, không sử dụng con trỏ đến nó"). Vì bản đồ là loại tham chiếu, bạn thu được gì khi đặt "Add ()" trở thành bộ thu con trỏ? Mọi bản sao của TokenCache sẽ tham chiếu đến cùng một bản đồ. Xem sân chơi cờ vây này
Phong phú

Rất vui vì chúng tôi đã liên kết. Điểm tuyệt vời. Trên thực tế, tôi nghĩ rằng tôi đã sử dụng con trỏ trong ví dụ này vì tôi đã sao chép nó từ một dự án mà TokenCache đang xử lý nhiều thứ hơn chỉ là bản đồ đó. Và nếu tôi sử dụng một con trỏ trong một phương thức, tôi sử dụng nó trong tất cả chúng. Bạn có đề xuất loại bỏ con trỏ khỏi ví dụ SO cụ thể này không?
Lukas Lukac

LOL, sao chép / dán cảnh cáo một lần nữa! 😉 IMO bạn có thể để nguyên vì nó minh họa một cái bẫy dễ rơi vào hoặc bạn có thể thay thế bản đồ bằng (các) thứ gì đó thể hiện trạng thái và / hoặc cấu trúc dữ liệu lớn.
phong phú

Chà, tôi chắc chắn họ sẽ đọc những bình luận ... Tái bút: Giàu có, lập luận của bạn có vẻ hợp lý, thêm tôi vào LinkedIn (liên kết trong hồ sơ của tôi) rất vui được kết nối.
Lukas Lukac
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.