Chốt

Báo cáo vấn đề Xem nguồn Nightly/3}

Tập hợp 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ột mục tiêu. Chúng là một yếu tố thiết yếu của quá trình xử lý quy tắc.

Đặc điểm xác định của depset là hoạt động hợp nhất tiết kiệm thời gian và không gian. Hàm khởi tạo phần phụ thuộc chấp nhận danh sách các phần tử ("trực tiếp") và danh sách các phần phụ thuộc khác ("chuyển tiếp") và trả về một phần phụ thuộc đạ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 của tất cả các tập bắc cầu. Về mặt lý thuyết, hàm khởi tạo sẽ tạo một nút biểu đồ mới, trong đó có các nút trực tiếp và nút bắc cầu là các nút kế thừa. Các phần phụ thuộc có ngữ nghĩa thứ tự được xác định rõ ràng, dựa trên việc truyền tải biểu đồ này.

Ví dụ về cách sử dụng phần phụ:

  • 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 đó, hệ thống có thể chuyển đường dẫn này đến hành động liên kết thông qua một trình cung cấp.

  • Đối với ngôn ngữ thông dịch, hãy lưu trữ các tệp nguồn bắc cầu có trong các tệp chạy của tệp thực thi.

Mô tả và hoạt động

Về mặt lý thuyết, tập hợp là một biểu đồ không chu trình có hướng (DAG) thường trông tương tự như biểu đồ mục tiêu. Nó được làm từ lá cho đến 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 ở trên nội dung trước đó mà không cần phải đọc hoặc sao chép chúng.

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 tập hợp 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 phần phụ thuộc mới bằng hàm khởi tạo depset: hàm này chấp nhận danh sách các phần tử trực tiếp và một danh sách các nút con khác.

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 phần phụ thuộc, hãy sử dụng phương thức to_list(). Phương thức này sẽ trả về danh sách 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 có ảnh hưởng đến thứ tự trả về các phần tử.

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 tập hợp 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 tách rời có thể không thay đổi được.

Các phần phụ sử dụng đẳng thức tham chiếu: một tập hợp có giá trị bằng chính nó nhưng không bằng bất kỳ tập nào khác, ngay cả khi các tập đó có cùng nội dung và cấu trúc bên trong giống nhau.

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 phần phụ thuộc theo nội dung, hãy chuyển đổi chúng thành danh sách được 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 tập hợp. Nếu cần, bạn phải đọc toàn bộ nội dung của phần phụ thuộc, lọc các phần tử bạn muốn xoá và tạo lại phần phụ thuộc mới. Điều này không đặc biệt hiệu quả.

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

Toán tử to_list thực hiện việc truyền tải qua DAG. Loại truyền tải phụ thuộc vào thứ tự được chỉ định tại thời điểm tạo phần tách. Bazel rất hữu ích khi hỗ trợ nhiều đơn đặt hàng vì đôi khi các công cụ quan tâm đến thứ tự đầu vào. Ví dụ: một thao tác của trình 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.

3 thứ tự truyền tải được hỗ trợ: postorder, preordertopological. Hai phần tử đầu tiên hoạt động giống hệt như truyền tải qua 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 cách sắp xếp cấu trúc liên kết từ gốc đến lá, về cơ bản giống như thứ tự đặt hàng trước, ngoại trừ việc phần tử con chung chỉ được liệt kê sau tất cả phần tử mẹ của chúng. Tính năng đặt hàng trước và sau thứ tự hoạt động như các hoạt động truyền tải từ trái sang phải, nhưng lưu ý rằng trong mỗi phần tử trực tiếp của nút không có thứ tự liên quan đến phần tử con. Đối với thứ tự cấu trúc liên kết, không có sự bảo đảm từ trái sang phải và thậm chí cả bảo đảm mẹ trước 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 hoạt động truyền tải, bạn phải chỉ định thứ tự tại thời điểm tạo phần tách bằng đối số từ khoá order của hàm khởi tạo. Nếu đối số này bị bỏ qua, tập hợp 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 (ngoại trừ việc phần tử đó mang tính tất đị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 Foo. Để tạo từng foo_binary, bạn cần biết tất cả các tệp *.foo mà phương thức đó trực tiếp hoặc gián tiếp phụ thuộc.

# //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 d nhị phân là tất cả các tệp *.foo trong trường srcs của a, b, cd. Để mục tiêu foo_binary biết về bất kỳ tệp nào ngoài d.foo, các mục tiêu foo_library cần truyền các tệp đó vào một trình cung cấp. Mỗi thư viện nhận các trình cung cấp từ các phần phụ thuộc riêng, thêm các nguồn tức thì riêng và chuyển sang một trình cung cấp mới có nội dung tăng cường. Quy tắc foo_binary cũng giống 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ử dụng danh sách đầy đủ các nguồn để tạo một dòng lệnh cho một thao tác.

Dưới đây là triển khai đầy đủ các quy tắc foo_libraryfoo_binary.

# //depsets/foo.bzl

# A provider with one field, transitive_sources.
FooFiles = 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[FooFiles].transitive_sources for dep in deps])

def _foo_library_impl(ctx):
  trans_srcs = get_transitive_srcs(ctx.files.srcs, ctx.attr.deps)
  return [FooFiles(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ử điều này 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 phù hợp, tạo tệp *.foo nguồn có nội dung giả và tạo mục tiêu d.

Hiệu suất

Để xem động lực sử dụng phần tách, hãy cân nhắc điều gì sẽ xảy ra nếu get_transitive_srcs() thu thập các nguồn trong một danh sách.

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

Điều này không tính đến các bản sao, vì vậy, các tệp nguồn của a sẽ xuất hiện 2 lần trên dòng lệnh và 2 lần trong nội dung của tệp đầu ra.

Một cách thay thế là sử dụng tập hợp chung, có thể được mô phỏng bằng từ điển, trong đó các khoá là các phần tử và tất cả các khoá ánh xạ đến True.

def get_transitive_srcs(srcs, deps):
  trans_srcs = {}
  for dep in deps:
    for file in dep[FooFiles].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 làm cho 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 này đều kém hiệu quả hơn so với phương pháp dựa trên phần cài đặt. Hãy xem xét trường hợp có một chuỗi phần phụ thuộc dài trên thư viện Foo. Việc xử lý mọi quy tắc yêu cầu sao chép tất cả các nguồn bắc cầu xuất hiện trước nó vào một cấu trúc dữ liệu mới. Điều này có nghĩa là thời gian và chi phí 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ệ với chiều cao của chính thư viện hoặc mục tiêu đó trong chuỗi. Đối với chuỗi có độ dài n, foolib_1 ← foolib_2 ← ... ← foolib_n, chi phí tổng thể có hiệu quả O(n^2).

Nói chung, bạn nên sử dụng phần lồng ghép 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 điều chỉnh theo tỷ lệ khi biểu đồ mục tiêu phát triển sâu hơn.

Cuối cùng, bạn không nên truy xuất nội dung của phần gỡ bỏ một cách không cần thiết trong quá trình triển khai quy tắc. Việc thực hiện một lệnh gọi đến to_list() ở cuối trong quy tắc nhị phân là không có vấn đề, vì tổng chi phí chỉ là O(n). Đó là khi nhiều mục tiêu không phải đầu cuối 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 hiệu quả phần tách, 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 chi tiết.