Depsets เป็นโครงสร้างข้อมูลเฉพาะทางที่ใช้รวบรวมข้อมูลข้ามการขึ้นต่อกันแบบส่งผ่านของเป้าหมายอย่างมีประสิทธิภาพ ซึ่งเป็นองค์ประกอบสำคัญ ของการประมวลผลกฎ
ฟีเจอร์ที่กำหนดของ depset คือการดำเนินการรวมที่ประหยัดเวลาและพื้นที่ ตัวสร้าง depset รับรายการองค์ประกอบ ("โดยตรง") และรายการ depset อื่นๆ ("แบบส่งผ่าน") แล้วแสดงผล depset ที่แสดงถึงชุดที่มีองค์ประกอบโดยตรงทั้งหมด และการรวมชุดแบบส่งผ่านทั้งหมด ในเชิงแนวคิด ตัวสร้างจะสร้างโหนดกราฟใหม่ที่มีโหนดโดยตรงและโหนดแบบส่งผ่าน เป็นโหนดสืบทอด Depsets มีความหมายที่กำหนดไว้อย่างชัดเจนเกี่ยวกับการจัดลำดับตาม การข้ามกราฟนี้
ตัวอย่างการใช้ depset ได้แก่
การจัดเก็บเส้นทางของไฟล์ออบเจ็กต์ทั้งหมดสำหรับไลบรารีของโปรแกรม ซึ่งสามารถ ส่งไปยังการดำเนินการ Linker ผ่านผู้ให้บริการ
สำหรับภาษาที่ตีความ การจัดเก็บไฟล์ต้นฉบับแบบส่งผ่านที่ รวมอยู่ในไฟล์ที่เรียกใช้ได้ของ Executable
คำอธิบายและการดำเนินการ
ในเชิงแนวคิด depset คือกราฟแบบมีทิศทางและไม่มีวงจร (DAG) ซึ่งโดยทั่วไปจะมีลักษณะคล้ายกับกราฟเป้าหมาย โดยสร้างจากโหนดปลายทางขึ้นไปยังโหนดราก เป้าหมายแต่ละรายการในเชนการขึ้นต่อกันสามารถเพิ่มเนื้อหาของตัวเองลงในเนื้อหาของ เป้าหมายก่อนหน้าได้โดยไม่ต้องอ่านหรือคัดลอกเนื้อหาเหล่านั้น
โหนดแต่ละโหนดใน DAG จะมีรายการองค์ประกอบโดยตรงและรายการโหนดย่อย เนื้อหาของ depset คือองค์ประกอบแบบส่งผ่าน เช่น องค์ประกอบโดยตรง ของโหนดทั้งหมด คุณสร้าง depset ใหม่ได้โดยใช้ตัวสร้าง depset ซึ่งรับรายการองค์ประกอบโดยตรง และรายการโหนดย่อยอีกรายการ
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"])
หากต้องการดึงข้อมูลเนื้อหาของ depset ให้ใช้เมธอด to_list() ซึ่งจะแสดงผลรายการองค์ประกอบแบบส่งผ่านทั้งหมดโดยไม่รวมรายการที่ซ้ำกัน คุณไม่สามารถตรวจสอบโครงสร้างที่แน่นอนของ DAG ได้โดยตรง แม้ว่าโครงสร้างนี้จะส่งผลต่อลำดับที่ระบบแสดงผลองค์ประกอบ
s = depset(["a", "b", "c"])
print("c" in s.to_list()) # True
print(s.to_list() == ["a", "b", "c"]) # True
รายการที่อนุญาตใน depset มีข้อจำกัดเช่นเดียวกับคีย์ที่อนุญาตใน พจนานุกรม โดยเฉพาะอย่างยิ่ง เนื้อหา depset อาจเปลี่ยนแปลงไม่ได้
Depsets ใช้ความเท่าเทียมกันของการอ้างอิง โดย depset จะเท่ากับตัวเอง แต่ไม่เท่ากับ depset อื่นๆ แม้ว่าจะมีเนื้อหาและโครงสร้างภายในเหมือนกันก็ตาม
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
หากต้องการเปรียบเทียบ depset ตามเนื้อหา ให้แปลง depset เป็นรายการที่จัดเรียงแล้ว
s = depset(["a", "b", "c"])
t = depset(["c", "b", "a"])
print(sorted(s.to_list()) == sorted(t.to_list())) # True
คุณไม่สามารถนำองค์ประกอบออกจาก depset ได้ หากต้องการดำเนินการดังกล่าว คุณ ต้องอ่านเนื้อหาทั้งหมดของ depset กรององค์ประกอบที่ต้องการนำออก และสร้าง depset ใหม่ ซึ่งอาจไม่มีประสิทธิภาพมากนัก
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"])
ลำดับ
การดำเนินการ to_list จะข้าม DAG ประเภทการข้าม
จะขึ้นอยู่กับ ลำดับ ที่ระบุไว้ในขณะที่สร้าง depset
การรองรับลำดับที่หลากหลายของ Bazel มีประโยชน์เนื่องจากบางครั้ง
เครื่องมือต่างๆ จะพิจารณาลำดับของอินพุต ตัวอย่างเช่น การดำเนินการ Linker อาจ
ต้องตรวจสอบว่าหาก B ขึ้นอยู่กับ A แล้ว A.o จะต้องมาก่อน B.o ใน
บรรทัดคำสั่งของ Linker เครื่องมืออื่นๆ อาจมีข้อกำหนดที่ตรงกันข้าม
ระบบรองรับลำดับการข้าม 3 แบบ ได้แก่ postorder, preorder และ
topological. 2 แบบแรกทำงานเหมือนกับการข้ามต้นไม้
ทุกประการ
ยกเว้นว่าจะดำเนินการกับ DAG และข้ามโหนดที่เข้าชมแล้ว ส่วนลำดับที่ 3
จะทำงานเป็นการจัดเรียงโทโพโลยีจากรากไปยังโหนดปลายทาง ซึ่งโดยพื้นฐานแล้วจะเหมือนกับ
ลำดับก่อนหน้า ยกเว้นว่าระบบจะแสดงรายการโหนดย่อยที่แชร์หลังจากโหนดหลักทั้งหมดเท่านั้น
ลำดับก่อนหน้าและลำดับหลังจะดำเนินการเป็นการข้ามจากซ้ายไปขวา แต่โปรดทราบว่าภายใน
องค์ประกอบโดยตรงแต่ละโหนดจะไม่มีลำดับที่สัมพันธ์กับโหนดย่อย สำหรับลำดับโทโพโลยี
จะไม่มีการรับประกันจากซ้ายไปขวา และแม้แต่การรับประกันว่าโหนดหลักทั้งหมดจะมาก่อนโหนดย่อยก็ใช้ไม่ได้ในกรณีที่มีองค์ประกอบที่ซ้ำกันในโหนดต่างๆ ของ 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"]
เนื่องจากวิธีที่ใช้ในการข้าม คุณจึงต้องระบุลำดับในขณะที่
สร้าง depset ด้วยอาร์กิวเมนต์คีย์เวิร์ด order ของตัวสร้าง หากละเว้นอาร์กิวเมนต์นี้
depset จะมีลำดับ default พิเศษ ซึ่งในกรณีนี้
จะไม่มีการรับประกันเกี่ยวกับลำดับขององค์ประกอบใดๆ (ยกเว้นว่าลำดับนั้น
จะกำหนดไว้)
ตัวอย่างฉบับเต็ม
ตัวอย่างนี้มีอยู่ที่ https://github.com/bazelbuild/examples/tree/main/rules/depsets
สมมติว่ามีภาษา Foo ที่ตีความได้ หากต้องการสร้าง
แต่ละ foo_binary คุณต้องทราบไฟล์ *.foo ทั้งหมดที่ขึ้นอยู่โดยตรงหรือ
โดยอ้อม
# //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())
ในที่นี้ แหล่งที่มาแบบส่งผ่านของไบนารี d คือไฟล์ *.foo ทั้งหมดใน
ช่อง srcs ของ a, b, c, และ d หากต้องการให้ foo_binary
เป้าหมายทราบเกี่ยวกับไฟล์อื่นๆ นอกเหนือจาก d.foo เป้าหมาย foo_library ต้อง
ส่งไฟล์เหล่านั้นในผู้ให้บริการ ไลบรารีแต่ละรายการจะได้รับผู้ให้บริการจากการขึ้นต่อกันของตัวเอง
เพิ่มแหล่งที่มาโดยตรงของตัวเอง และส่งต่อผู้ให้บริการรายใหม่ที่มี
เนื้อหาเพิ่มเติม กฎ foo_binary จะทำเช่นเดียวกัน ยกเว้นว่ากฎนี้จะใช้รายการแหล่งที่มาทั้งหมดเพื่อสร้าง
บรรทัดคำสั่งสำหรับการดำเนินการแทนที่จะแสดงผลผู้ให้บริการ
นี่คือการใช้งานกฎ foo_library และ foo_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"},
)
คุณสามารถทดสอบได้โดยคัดลอกไฟล์เหล่านี้ลงในแพ็กเกจใหม่ เปลี่ยนชื่อ
ป้ายกำกับให้เหมาะสม สร้างไฟล์ต้นฉบับ *.foo ที่มีเนื้อหาจำลอง และ
สร้างเป้าหมาย d
ประสิทธิภาพ
หากต้องการดูเหตุผลในการใช้ depset ให้พิจารณาว่าจะเกิดอะไรขึ้นหาก
get_transitive_srcs() รวบรวมแหล่งที่มาในรายการ
def get_transitive_srcs(srcs, deps):
trans_srcs = []
for dep in deps:
trans_srcs += dep[FooFiles].transitive_sources
trans_srcs += srcs
return trans_srcs
วิธีนี้ไม่ได้พิจารณาถึงรายการที่ซ้ำกัน ดังนั้นไฟล์ต้นฉบับสำหรับ a
จะปรากฏ 2 ครั้งในบรรทัดคำสั่งและ 2 ครั้งในเนื้อหาของไฟล์เอาต์พุต
อีกทางเลือกหนึ่งคือการใช้ชุดทั่วไป ซึ่งสามารถจำลองได้ด้วยa
พจนานุกรมที่คีย์เป็นองค์ประกอบและคีย์ทั้งหมดแมปกับ 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
วิธีนี้จะกำจัดรายการที่ซ้ำกัน แต่จะทำให้ลำดับของอาร์กิวเมนต์บรรทัดคำสั่ง (และเนื้อหาของไฟล์) ไม่ได้ระบุไว้ แม้ว่าลำดับนั้นจะยังคงกำหนดไว้ก็ตาม
นอกจากนี้ ทั้ง 2 วิธีนี้ยังแย่กว่าวิธีที่ใช้ depset ในเชิงเส้นกำกับ ลองพิจารณากรณีที่มีการขึ้นต่อกันแบบเชนยาวในไลบรารี Foo การประมวลผลกฎแต่ละข้อต้องใช้การคัดลอกแหล่งที่มาแบบส่งผ่านทั้งหมดที่มาก่อนหน้านี้ลงในโครงสร้างข้อมูลใหม่ ซึ่งหมายความว่าค่าใช้จ่ายด้านเวลาและพื้นที่ในการวิเคราะห์ไลบรารีหรือเป้าหมายไบนารีแต่ละรายการจะแปรผันตามความสูงของรายการนั้นในเชน สำหรับเชนที่มีความยาว n, foolib_1 ← foolib_2 ← … ← foolib_n ค่าใช้จ่ายโดยรวมจะเป็น O(n^2)
โดยทั่วไป คุณควรใช้ depset ทุกครั้งที่สะสม ข้อมูลผ่านการขึ้นต่อกันแบบส่งผ่าน ซึ่งจะช่วยให้ การสร้างของคุณปรับขนาดได้ดีเมื่อกราฟเป้าหมายลึกขึ้น
สุดท้ายนี้ คุณไม่ควรดึงข้อมูลเนื้อหาของ depset
โดยไม่จำเป็นในการใช้งานกฎ การเรียก to_list()
1 ครั้งที่ส่วนท้ายในกฎไบนารีถือว่าใช้ได้ เนื่องจากค่าใช้จ่ายโดยรวมเป็นเพียง O(n) แต่หากเป้าหมายที่ไม่ใช่เป้าหมายปลายทางหลายรายการพยายามเรียก to_list() จะเกิดลักษณะการทำงานแบบกำลังสอง
ดูข้อมูลเพิ่มเติมเกี่ยวกับการใช้ depset อย่างมีประสิทธิภาพได้ที่หน้า ประสิทธิภาพ
เอกสารอ้างอิง API
โปรดดูรายละเอียดเพิ่มเติมที่ นี่