Binding Lua Cho Thư Viện Pbc - nói dối e blog

Binding Lua Cho Thư Viện Pbc

Một trong những mục tiêu ban đầu khi tôi bắt đầu phát triển thư viện pbc cách đây vài ngày chính là tạo điều kiện thuận lợi cho việc tích hợp với các ngôn ngữ kịch bản (dynamic language). Chính vì vậy hôm nay tôi dành trọn một ngày để tự viết một thư viện binding Lua đơn giản, công việc này diễn ra khá tự nhiên và thuận lợi.

Sau khi hoàn tất, tôi rất tò mò về hiệu năng nên đã viết một chương trình benchmark đơn giản để kiểm thử. Cần lưu ý rằng kết quả thử nghiệm này chưa thể phản ánh đầy đủ vì dữ liệu kiểm thử quá đơn giản. Tôi sẽ tìm kiếm một bộ dữ liệu phức tạp hơn vào ngày mai để đánh giá toàn diện hơn.

Điều khiến tôi bất ngờ là tại sao bản triển khai C++ chính thức của Google lại có hiệu năng kém đến vậy. Đoạn code Lua dùng thử có dạng:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
local protobuf = require "protobuf"
addr = io.open("../../build/addressbook.pb","rb")
buffer = addr:read "*a"
addr:close()
protobuf.register(buffer)
for i=1,1000000 do
  local person = {
    name = "Alice",
    id = 123,
  }
  local buffer = protobuf.encode("tutorial.Person", person)
  local t = protobuf.decode("tutorial.Person", buffer)
end

Với 1 triệu phép encode/decode, trên máy tôi hiện tại mất 3.8 giây. Để đáp ứng các tình huống yêu cầu hiệu năng cao, tôi đã thiết kế một nhóm API đặc biệt với cơ chế “flat serialization” giúp tránh việc tạo lập bảng (table) Lua truyền thống. Thay đổi nhỏ trong vòng lặp:

1
2
3
4
  local buffer = protobuf.pack(
    "tutorial.Person name id",
     "Alice", 123)
  protobuf.unpack("tutorial.Person name id", buffer)

Chỉ cần thay đổi này, thời gian giảm xuống còn 0.9 giây. Một tháng trước, tôi từng viết một bản pure Lua dùng luajit + ffi (chưa công khai), chạy thử với kịch bản này đạt 2.1 giây.

Tôi tin rằng độ trễ chủ yếu phát sinh từ môi trường Lua. Khi kích hoạt luajit, hiệu năng cải thiện rõ rệt:

  • Phiên bản table: 1.7 giây
  • Phiên bản flat: 0.57 giây

Hiệu quả tối ưu hóa từ luajit thực sự ấn tượng. Nhân tiện, tôi cũng đã viết một thư viện binding Lua vào năm ngoái, chạy thử với luajit hiện tại chỉ mất 1.2 giây - nhanh hơn bản cũ nhưng chưa bằng bản mới viết gần đây.

Cuối cùng, tôi thử so sánh với bản C++ tiêu chuẩn:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>
#include <fstream>
#include <sstream>
#include "addressbook.pb.h"

using namespace std;

int main(int argc, char* argv[]) {
  GOOGLE_PROTOBUF_VERIFY_VERSION;
  for (int i=0; i<1000000; i++) {
    tutorial::Person person;
    person.set_name("Alice");
    person.set_id(123);
    stringstream output;
    person.SerializeToOstream(&output);
    output.str();
    tutorial::Person person2;
    person2.ParseFromIstream(&output);
    person.name(); person.id();
  }
  google::protobuf::ShutdownProtobufLibrary();
  return 0;
}

Phiên bản biên dịch với -O2 vẫn mất 1.9 giây - quá chậm so với bản binding C/LuaJIT. Mặc dù có thể còn nhiều hướng tối ưu, nhưng đây là cách triển khai phổ biến nhất. Một bạn đọc tên lifc0 nhận xét rằng thời gian bị tiêu tốn chủ yếu cho việc khởi tạo/destory stringstream. Tôi đã tối ưu bằng cách tái sử dụng stream:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
stringstream output;
stringstream input;

for (int i=0; i<1000000; i++) {
  output.clear();
  output.str("");
  tutorial::Person person;
  person.set_name("Alice");
  person.set_id(123);
  person.SerializeToOstream(&output);
  input.clear();
  input.str(output.str());
  tutorial::Person person2;
  person2.ParseFromIstream(&input);
  person2.name(); person2.id();
}

Chỉ cần điều chỉnh cách khởi tạo stream mà thời gian giảm từ 1.90s xuống còn 1.18s, gần với thực tiễn hơn nhiều. Nếu bạn có bất kỳ phân tích nào liên quan đến hiệu năng của C++ trong trường hợp này, tôi rất mong được trao đổi chi tiết hơn.

Ngày 16 tháng 12

0%