2016-06-03 35 views
6

Tôi đã được giao nhiệm vụ thay thế mã C++ thành Go và tôi khá mới đối với API Go. Tôi đang sử dụng gob để mã hóa hàng trăm khóa/giá trị mục vào các trang đĩa nhưng mã hóa gob có quá nhiều bloat không cần thiết.Hiệu quả Tuần tự hóa cấu trúc vào đĩa

package main 

import (
    "bytes" 
    "encoding/gob" 
    "fmt" 
) 
type Entry struct { 
    Key string 
    Val string 
} 

func main() { 
    var buf bytes.Buffer 
    enc := gob.NewEncoder(&buf) 
    e := Entry { "k1", "v1" } 
    enc.Encode(e) 
    fmt.Println(buf.Bytes()) 
} 

này tạo ra rất nhiều sưng lên mà tôi không cần:

[35 255 129 3 1 1 5 69 110 116 114 121 1 255 130 0 1 2 1 3 75 101 121 1 12 0 1 3 86 97 108 1 12 0 0 0 11 255 130 1 2 107 49 1 2 118 49 0] 

Tôi muốn serialize len mỗi chuỗi tiếp theo là byte thô như:

[0 0 0 2 107 49 0 0 0 2 118 49] 

Tôi tiết kiệm hàng triệu mục nhập để bloat bổ sung trong mã hóa tăng kích thước tệp lên khoảng x10.

Làm cách nào để có thể tuần tự hóa nó sau này mà không cần mã hóa thủ công?

Trả lời

8

Sử dụng protobuf để mã hóa dữ liệu của bạn một cách hiệu quả.

https://github.com/golang/protobuf

của bạn chính sẽ trông như thế này:

package main 

import (
    "fmt" 
    "log" 

    "github.com/golang/protobuf/proto" 
) 

func main() { 
    e := &Entry{ 
     Key: proto.String("k1"), 
     Val: proto.String("v1"), 
    } 
    data, err := proto.Marshal(e) 
    if err != nil { 
     log.Fatal("marshaling error: ", err) 
    } 
    fmt.Println(data) 
} 

Bạn tạo một tập tin, example.proto như thế này:

package main; 

message Entry { 
    required string Key = 1; 
    required string Val = 2; 
} 

Bạn tạo mã đi từ file proto bằng cách chạy:

$ protoc --go_out=. *.proto 

Bạn có thể kiểm tra tệp được tạo, nếu bạn muốn.

Bạn có thể chạy và xem kết quả đầu ra:

$ go run *.go 
[10 2 107 49 18 2 118 49] 
15

Nếu bạn nén một file có tên a.txt chứa văn bản "hello" (đó là 5 ký tự), kết quả zip sẽ đạt khoảng 115 byte. Điều này có nghĩa là định dạng zip không hiệu quả để nén tệp văn bản không? Chắc chắn không. Có chi phí trên không. Nếu tệp có chứa "hello" một trăm lần (500 byte), nén nó sẽ dẫn đến tệp là 120 byte! 1x"hello" => 115 byte, 100x"hello" => 120 byte! Chúng tôi đã thêm 495 lượt, và kích thước nén chỉ tăng 5 byte.

Something tương tự đang xảy ra với gói encoding/gob:

Việc thực hiện biên dịch một codec tùy chỉnh cho mỗi kiểu dữ liệu trong dòng và là hiệu quả nhất khi một bộ mã hóa duy nhất được sử dụng để truyền một dòng giá trị, trả dần chi phí biên dịch.

Khi bạn "đầu tiên" serialize một giá trị của một loại, nét của các loại cũng đã được đưa/truyền, vì vậy các bộ giải mã đúng cách có thể giải thích và giải mã các dòng:

Một dòng gobs là tự mô tả.Mỗi mục dữ liệu trong luồng được bắt đầu bằng một đặc điểm kỹ thuật của loại dữ liệu của nó, được biểu thị dưới dạng một tập hợp nhỏ các loại được xác định trước.

Hãy trở lại với ví dụ của bạn:

var buf bytes.Buffer 
enc := gob.NewEncoder(&buf) 
e := Entry{"k1", "v1"} 
enc.Encode(e) 
fmt.Println(buf.Len()) 

It in:

48 

Bây giờ chúng ta hãy mã hóa một vài chi tiết của các loại cùng:

enc.Encode(e) 
fmt.Println(buf.Len()) 
enc.Encode(e) 
fmt.Println(buf.Len()) 

Bây giờ đầu ra là:

60 
72 

Hãy thử trên Go Playground.

Phân tích kết quả:

giá trị bổ sung của cùng Entry loại chỉ có giá 12 byte, trong khi người đầu tiên là 48 byte bởi vì định nghĩa kiểu cũng được bao gồm (đó là ~ 26 byte), nhưng đó là một chi phí một lần.

Vì vậy, về cơ bản bạn chuyển 2 string s: "k1""v1" là 4 byte, và độ dài của string s cũng phải được bao gồm, sử dụng 4 byte (kích thước của int trên kiến ​​trúc 32-bit) cung cấp cho bạn 12 byte , đó là "tối thiểu". (Có, bạn có thể sử dụng loại nhỏ hơn cho độ dài, nhưng điều đó sẽ có những hạn chế của nó. Mã hóa có độ dài biến đổi sẽ là lựa chọn tốt hơn cho số nhỏ, xem gói encoding/binary.)

Tất cả trong tất cả, encoding/gob công việc tốt cho nhu cầu của bạn. Đừng bị lừa bởi những ấn tượng ban đầu.

Nếu đây 12 byte cho một Entry là quá yêu cầu bộ nhớ cao hơn một chút cho "nhiều" cho bạn, bạn luôn có thể quấn dòng thành một compress/flate hoặc compress/gzip nhà văn để tiếp tục giảm kích thước (để đổi lấy chậm mã hóa/giải mã và quá trình).

diễn:

Hãy kiểm tra 3 giải pháp:

  • Sử dụng một "trần truồng" đầu ra (không nén)
  • Sử dụng compress/flate để nén đầu ra của encoding/gob
  • Sử dụng compress/gzip để nén đầu ra của encoding/gob

Chúng tôi sẽ viết một ngàn mục, thay đổi các phím và các giá trị của mỗi, là "k000", "v000", "k001", "v001" vv Điều này có nghĩa là kích thước không nén của một Entry là 4 byte + 4 byte + 4 byte + 4 byte = 16 byte (văn bản 2x 4 byte, độ dài byte 2x4).

Mã này trông như thế này:

names := []string{"Naked", "flate", "gzip"} 
for _, name := range names { 
    buf := &bytes.Buffer{} 

    var out io.Writer 
    switch name { 
    case "Naked": 
     out = buf 
    case "flate": 
     out, _ = flate.NewWriter(buf, flate.DefaultCompression) 
    case "gzip": 
     out = gzip.NewWriter(buf) 
    } 

    enc := gob.NewEncoder(out) 
    e := Entry{} 
    for i := 0; i < 1000; i++ { 
     e.Key = fmt.Sprintf("k%3d", i) 
     e.Val = fmt.Sprintf("v%3d", i) 
     enc.Encode(e) 
    } 

    if c, ok := out.(io.Closer); ok { 
     c.Close() 
    } 
    fmt.Printf("[%5s] Length: %5d, average: %5.2f/Entry\n", 
     name, buf.Len(), float64(buf.Len())/1000) 
} 

Output:

[Naked] Length: 16036, average: 16.04/Entry 
[flate] Length: 4123, average: 4.12/Entry 
[ gzip] Length: 4141, average: 4.14/Entry 

Hãy thử nó trên Go Playground.

Như bạn có thể thấy: đầu ra "trần truồng" là 16.04 bytes/Entry, chỉ nhỏ hơn kích thước được tính toán (do chi phí nhỏ một lần đã thảo luận ở trên).

Khi bạn sử dụng flate hoặc gzip để nén đầu ra, bạn có thể giảm kích thước đầu ra xuống khoảng 4.13 bytes/Entry, khoảng ~ 26% kích thước lý thuyết, tôi chắc chắn rằng sẽ thỏa mãn bạn. (Lưu ý rằng với dữ liệu "cuộc sống thực", tỷ lệ nén có thể cao hơn rất nhiều vì các khóa và giá trị tôi đã sử dụng trong thử nghiệm rất giống nhau và do đó thực sự được nén tốt, tỷ lệ vẫn là khoảng 50% với dữ liệu thực tế).

+1

Phân tích ấn tượng (Tôi luôn ngưỡng mộ câu trả lời của bạn) nhưng trong trường hợp cụ thể này có vẻ như giải thích khoa học tên lửa cho một đứa trẻ hỏi tại sao chiếc xe ba bánh của anh hơi chậm. ;-) Trong khi tôi nghĩ rằng 'gob' chắc chắn đã sử dụng của nó, cho nhiệm vụ đơn giản như vậy trong tay OP dường như có, tôi chắc chắn rằng một reimplementation đơn giản của những gì đã được thực hiện trong C + + được bảo hành. Một ưu điểm khác của phương pháp này là mã mới sẽ được so sánh với dữ liệu kế thừa mà chúng có. – kostix

+0

@ kostix Đó là suy nghĩ và ấn tượng đầu tiên của tôi về câu hỏi, nhưng sau đó tôi thấy dòng cuối cùng của nó: _ "không có mã hóa thủ công" _... Vì vậy, đó là lý do tôi quyết định ở lại với 'encoding/gob'. – icza

3

"Mã hóa thủ công", bạn rất sợ, được thực hiện một cách travially trong Go bằng cách sử dụng tiêu chuẩn encoding/binary package.

Bạn xuất hiện để lưu trữ các giá trị chiều dài chuỗi như số nguyên 32-bit ở định dạng lớn về cuối nhỏ, vì vậy bạn chỉ có thể tiếp tục và làm việc đó tại Gò:

package main 

import (
    "bytes" 
    "encoding/binary" 
    "fmt" 
    "io" 
) 

func encode(w io.Writer, s string) (n int, err error) { 
    var hdr [4]byte 
    binary.BigEndian.PutUint32(hdr[:], uint32(len(s))) 
    n, err = w.Write(hdr[:]) 
    if err != nil { 
     return 
    } 
    n2, err := io.WriteString(w, s) 
    n += n2 
    return 
} 

func main() { 
    var buf bytes.Buffer 

    for _, s := range []string{ 
     "ab", 
     "cd", 
     "de", 
    } { 
     _, err := encode(&buf, s) 
     if err != nil { 
      panic(err) 
     } 
    } 
    fmt.Printf("%v\n", buf.Bytes()) 
} 

Playground link. Lưu ý rằng trong ví dụ này tôi đang viết cho một bộ đệm byte, nhưng đó là cho mục đích trình diễn chỉ — kể từ encode() ghi vào io.Writer, bạn có thể chuyển nó thành tệp mở, ổ cắm mạng và bất cứ thứ gì khác đang triển khai giao diện đó.

+0

Sau khi bình luận của bạn, tôi thậm chí còn muốn đề xuất tiếp tục và đăng phiên bản "thủ công". +1. – icza

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