Chốt

Báo cáo vấn đề Xem nguồn Nightly · 8.3 · 8.2 · 8.1 · 8.0 · 7.6

Depset là một cấu trúc dữ liệu chuyên biệt để thu thập dữ liệu một cách hiệu quả trên các phần phụ thuộc bắc cầu của mục tiêu. Đây là một yếu tố thiết yếu trong quá trình xử lý quy tắc.

Đặc điểm xác định của depset là thao tác hợp nhất hiệu quả về thời gian và không gian. Hàm khởi tạo depset chấp nhận một danh sách các phần tử ("trực tiếp") và một danh sách các depset khác ("bắc cầu"), đồng thời trả về một depset đại diện cho một tập hợp chứa tất cả các phần tử trực tiếp và hợp nhất tất cả các tập hợp bắc cầu. Về mặt khái niệm, hàm khởi tạo sẽ tạo một nút biểu đồ mới có các nút trực tiếp và bắc cầu làm nút kế thừa. Depsets có ngữ nghĩa sắp xếp được xác định rõ, dựa trên việc truyền tải biểu đồ này.

Sau đây là ví dụ về cách sử dụng depset:

  • Lưu trữ đường dẫn của tất cả các tệp đối tượng cho thư viện của chương trình, sau đó có thể được truyền đến một thao tác liên kết thông qua một trình cung cấp.

  • Đối với một ngôn ngữ được diễn giải, việc lưu trữ các tệp nguồn bắc cầu có trong tệp thực thi của runfiles.

Nội dung mô tả và hoạt động

Về mặt khái niệm, depset là một đồ thị có hướng không chu trình (DAG) thường trông giống với đồ thị mục tiêu. Cây này được tạo từ các nút lá cho đến nút gốc. Mỗi mục tiêu trong chuỗi phần phụ thuộc có thể thêm nội dung riêng lên trên nội dung trước đó mà không cần đọc hoặc sao chép nội dung đó.

Mỗi nút trong DAG chứa một danh sách các phần tử trực tiếp và một danh sách các nút con. Nội dung của depset là các phần tử bắc cầu, chẳng hạn như các phần tử trực tiếp của tất cả các nút. Bạn có thể tạo một depset mới bằng hàm khởi tạo depset: hàm này chấp nhận một danh sách các phần tử trực tiếp và một danh sách khác gồm các nút con.

s = depset(["a", "b", "c"])
t = depset(["d", "e"], transitive = [s])

print(s)    # depset(["a", "b", "c"])
print(t)    # depset(["d", "e", "a", "b", "c"])

Để truy xuất nội dung của một depset, hãy sử dụng phương thức to_list(). Phương thức này trả về một danh sách gồm tất cả các phần tử bắc cầu, không bao gồm các phần tử trùng lặp. Không có cách nào để kiểm tra trực tiếp cấu trúc chính xác của DAG, mặc dù cấu trúc này ảnh hưởng đến thứ tự mà các phần tử được trả về.

s = depset(["a", "b", "c"])

print("c" in s.to_list())              # True
print(s.to_list() == ["a", "b", "c"])  # True

Các mục được phép trong một depset bị hạn chế, giống như các khoá được phép trong từ điển cũng bị hạn chế. Cụ thể, nội dung của depset không được phép thay đổi.

Depsets sử dụng đẳng thức tham chiếu: một depset bằng chính nó, nhưng không bằng bất kỳ depset nào khác, ngay cả khi chúng có cùng nội dung và cùng cấu trúc nội bộ.

s = depset(["a", "b", "c"])
t = s
print(s == t)  # True

t = depset(["a", "b", "c"])
print(s == t)  # False

d = {}
d[s] = None
d[t] = None
print(len(d))  # 2

Để so sánh các depsets theo nội dung, hãy chuyển đổi chúng thành danh sách đã sắp xếp.

s = depset(["a", "b", "c"])
t = depset(["c", "b", "a"])
print(sorted(s.to_list()) == sorted(t.to_list()))  # True

Không thể xoá các phần tử khỏi depset. Nếu cần, bạn phải đọc toàn bộ nội dung của depset, lọc các phần tử bạn muốn xoá và tạo lại một depset mới. Điều này không hiệu quả lắm.

s = depset(["a", "b", "c"])
t = depset(["b", "c"])

# Compute set difference s - t. Precompute t.to_list() so it's not done
# in a loop, and convert it to a dictionary for fast membership tests.
t_items = {e: None for e in t.to_list()}
diff_items = [x for x in s.to_list() if x not in t_items]
# Convert back to depset if it's still going to be used for union operations.
s = depset(diff_items)
print(s)  # depset(["a"])

Đặt

Thao tác to_list thực hiện một lượt di chuyển trên DAG. Loại hoạt động duyệt qua phụ thuộc vào order được chỉ định tại thời điểm depset được tạo. Bazel nên hỗ trợ nhiều thứ tự vì đôi khi các công cụ quan tâm đến thứ tự của dữ liệu đầu vào. Ví dụ: một thao tác liên kết có thể cần đảm bảo rằng nếu B phụ thuộc vào A, thì A.o sẽ xuất hiện trước B.o trên dòng lệnh của trình liên kết. Các công cụ khác có thể có yêu cầu ngược lại.

Có 3 thứ tự duyệt được hỗ trợ: postorder, preordertopological. Hai phương pháp đầu tiên hoạt động giống hệt như các phương pháp duyệt cây, ngoại trừ việc chúng hoạt động trên DAG và bỏ qua các nút đã truy cập. Thứ tự thứ ba hoạt động như một sắp xếp theo cấu trúc liên kết từ gốc đến lá, về cơ bản giống như thứ tự trước, ngoại trừ việc các phần tử con dùng chung chỉ được liệt kê sau tất cả các phần tử mẹ của chúng. Thứ tự trước và thứ tự sau hoạt động như các lượt duyệt từ trái sang phải, nhưng lưu ý rằng trong mỗi nút, các phần tử trực tiếp không có thứ tự tương ứng với các phần tử con. Đối với thứ tự liên kết, không có đảm bảo từ trái sang phải và ngay cả đảm bảo tất cả các phần tử mẹ trước phần tử con cũng không áp dụng trong trường hợp có các phần tử trùng lặp trong các nút khác nhau của DAG.

# This demonstrates different traversal orders.

def create(order):
  cd = depset(["c", "d"], order = order)
  gh = depset(["g", "h"], order = order)
  return depset(["a", "b", "e", "f"], transitive = [cd, gh], order = order)

print(create("postorder").to_list())  # ["c", "d", "g", "h", "a", "b", "e", "f"]
print(create("preorder").to_list())   # ["a", "b", "e", "f", "c", "d", "g", "h"]
# This demonstrates different orders on a diamond graph.

def create(order):
  a = depset(["a"], order=order)
  b = depset(["b"], transitive = [a], order = order)
  c = depset(["c"], transitive = [a], order = order)
  d = depset(["d"], transitive = [b, c], order = order)
  return d

print(create("postorder").to_list())    # ["a", "b", "c", "d"]
print(create("preorder").to_list())     # ["d", "b", "a", "c"]
print(create("topological").to_list())  # ["d", "b", "c", "a"]

Do cách triển khai các lượt truyền tải, bạn phải chỉ định thứ tự tại thời điểm depset được tạo bằng đối số từ khoá order của hàm khởi tạo. Nếu bạn bỏ qua đối số này, depset sẽ có thứ tự default đặc biệt. Trong trường hợp đó, không có gì đảm bảo về thứ tự của bất kỳ phần tử nào trong số đó (ngoại trừ việc thứ tự đó là xác định).

Ví dụ đầy đủ

Ví dụ này có tại https://github.com/bazelbuild/examples/tree/main/rules/depsets.

Giả sử có một ngôn ngữ thông dịch giả định là Foo. Để tạo từng foo_binary, bạn cần biết tất cả các tệp *.foo mà tệp đó phụ thuộc trực tiếp hoặc gián tiếp.

# //depsets:BUILD

load(":foo.bzl", "foo_library", "foo_binary")

# Our hypothetical Foo compiler.
py_binary(
    name = "foocc",
    srcs = ["foocc.py"],
)

foo_library(
    name = "a",
    srcs = ["a.foo", "a_impl.foo"],
)

foo_library(
    name = "b",
    srcs = ["b.foo", "b_impl.foo"],
    deps = [":a"],
)

foo_library(
    name = "c",
    srcs = ["c.foo", "c_impl.foo"],
    deps = [":a"],
)

foo_binary(
    name = "d",
    srcs = ["d.foo"],
    deps = [":b", ":c"],
)
# //depsets:foocc.py

# "Foo compiler" that just concatenates its inputs to form its output.
import sys

if __name__ == "__main__":
  assert len(sys.argv) >= 1
  output = open(sys.argv[1], "wt")
  for path in sys.argv[2:]:
    input = open(path, "rt")
    output.write(input.read())

Ở đây, các nguồn bắc cầu của tệp nhị phân d là tất cả các tệp *.foo trong các trường srcs của a, b, cd. Để đích foo_binary biết về bất kỳ tệp nào ngoài d.foo, các đích foo_library cần truyền các tệp đó trong một trình cung cấp. Mỗi thư viện nhận các nhà cung cấp từ các phần phụ thuộc của riêng mình, thêm các nguồn trực tiếp của riêng mình và truyền một nhà cung cấp mới với nội dung được tăng cường. Quy tắc foo_binary cũng làm như vậy, ngoại trừ việc thay vì trả về một trình cung cấp, quy tắc này sẽ sử dụng danh sách đầy đủ các nguồn để tạo một dòng lệnh cho một hành động.

Sau đây là quy trình triển khai đầy đủ các quy tắc foo_libraryfoo_binary.

# //depsets/foo.bzl

# A provider with one field, transitive_sources.
foo_files = provider(fields = ["transitive_sources"])

def get_transitive_srcs(srcs, deps):
  """Obtain the source files for a target and its transitive dependencies.

  Args:
    srcs: a list of source files
    deps: a list of targets that are direct dependencies
  Returns:
    a collection of the transitive sources
  """
  return depset(
        srcs,
        transitive = [dep[foo_files].transitive_sources for dep in deps])

def _foo_library_impl(ctx):
  trans_srcs = get_transitive_srcs(ctx.files.srcs, ctx.attr.deps)
  return [foo_files(transitive_sources=trans_srcs)]

foo_library = rule(
    implementation = _foo_library_impl,
    attrs = {
        "srcs": attr.label_list(allow_files=True),
        "deps": attr.label_list(),
    },
)

def _foo_binary_impl(ctx):
  foocc = ctx.executable._foocc
  out = ctx.outputs.out
  trans_srcs = get_transitive_srcs(ctx.files.srcs, ctx.attr.deps)
  srcs_list = trans_srcs.to_list()
  ctx.actions.run(executable = foocc,
                  arguments = [out.path] + [src.path for src in srcs_list],
                  inputs = srcs_list + [foocc],
                  outputs = [out])

foo_binary = rule(
    implementation = _foo_binary_impl,
    attrs = {
        "srcs": attr.label_list(allow_files=True),
        "deps": attr.label_list(),
        "_foocc": attr.label(default=Label("//depsets:foocc"),
                             allow_files=True, executable=True, cfg="host")
    },
    outputs = {"out": "%{name}.out"},
)

Bạn có thể kiểm thử bằng cách sao chép các tệp này vào một gói mới, đổi tên nhãn một cách thích hợp, tạo tệp nguồn *.foo có nội dung giả và tạo mục tiêu d.

Hiệu suất

Để biết lý do sử dụng depsets, hãy xem xét điều gì sẽ xảy ra nếu get_transitive_srcs() thu thập các nguồn của nó trong một danh sách.

def get_transitive_srcs(srcs, deps):
  trans_srcs = []
  for dep in deps:
    trans_srcs += dep[foo_files].transitive_sources
  trans_srcs += srcs
  return trans_srcs

Thao tác này không tính đến các tệp trùng lặp, vì vậy, các tệp nguồn cho a sẽ xuất hiện hai lần trên dòng lệnh và hai lần trong nội dung của tệp đầu ra.

Một lựa chọn khác là sử dụng một tập hợp chung. Bạn có thể mô phỏng tập hợp này bằng một từ điển, trong đó các khoá là các phần tử và tất cả các khoá đều liên kết đến True.

def get_transitive_srcs(srcs, deps):
  trans_srcs = {}
  for dep in deps:
    for file in dep[foo_files].transitive_sources:
      trans_srcs[file] = True
  for file in srcs:
    trans_srcs[file] = True
  return trans_srcs

Thao tác này sẽ loại bỏ các bản sao, nhưng sẽ khiến thứ tự của các đối số dòng lệnh (và do đó, nội dung của các tệp) không được chỉ định, mặc dù vẫn mang tính xác định.

Hơn nữa, cả hai phương pháp đều tệ hơn phương pháp dựa trên depset. Hãy xem xét trường hợp có một chuỗi dài các phần phụ thuộc trên các thư viện Foo. Để xử lý mọi quy tắc, bạn cần sao chép tất cả các nguồn bắc cầu xuất hiện trước đó vào một cấu trúc dữ liệu mới. Điều này có nghĩa là chi phí thời gian và không gian để phân tích một thư viện hoặc mục tiêu nhị phân riêng lẻ tỷ lệ thuận với chiều cao của chính thư viện hoặc mục tiêu đó trong chuỗi. Đối với một chuỗi có độ dài n, foolib_1 ← foolib_2 ← … ← foolib_n, chi phí tổng thể là O(n^2).

Nói chung, bạn nên sử dụng depsets bất cứ khi nào bạn đang tích luỹ thông tin thông qua các phần phụ thuộc bắc cầu. Điều này giúp đảm bảo bản dựng của bạn có thể mở rộng khi biểu đồ mục tiêu của bạn phát triển sâu hơn.

Cuối cùng, điều quan trọng là bạn không được truy xuất nội dung của depset một cách không cần thiết trong quá trình triển khai quy tắc. Một lệnh gọi đến to_list() ở cuối quy tắc nhị phân là đủ, vì chi phí tổng thể chỉ là O(n). Đó là khi nhiều mục tiêu không phải là mục tiêu cuối cùng cố gắng gọi to_list() thì hành vi bậc hai sẽ xảy ra.

Để biết thêm thông tin về cách sử dụng depset một cách hiệu quả, hãy xem trang hiệu suất.

Tài liệu tham khảo API

Vui lòng xem tại đây để biết thêm thông tin.