1 điểm bởi GN⁺ 2023-10-06 | 1 bình luận | Chia sẻ qua WhatsApp
  • Theo dõi thông lượng của đường ống Linux bằng perf và phân tích đường đi trong kernel, từ bản triển khai đơn giản dựa trên write/read đạt 3.7GiB/s lên tới 62.5GiB/s sau tối ưu hóa
  • Điểm khởi đầu của nút thắt là pipe hoạt động như một ring buffer chứa các tham chiếu trang 4KiB, nên phải đồng thời trả chi phí sao chép, cấp phát trang, khóa, chờ và đánh thức
  • vmsplicesplice giảm sao chép giữa bộ nhớ người dùng và bộ đệm kernel, nâng thông lượng lần lượt lên khoảng 12.7GiB/s32.8GiB/s
  • Sau đó, chi phí chuyển đổi dải địa chỉ ảo thành struct page trở nên nổi bật, và dùng huge page 2MiB giúp giảm gánh nặng dò bảng trang, đưa thông lượng lên 51.0GiB/s
  • Tối ưu hóa cuối cùng với SPLICE_F_NONBLOCKbusy loop còn đẩy thông lượng cao hơn nữa, nhưng sẽ tiếp tục chiếm dụng lõi CPU trong lúc chờ trạng thái sẵn sàng

Mục tiêu thí nghiệm và hiệu năng cơ sở

  • Mục tiêu là tìm hiểu Unix pipe được triển khai như thế nào trên Linux, rồi tối ưu hóa từng bước một chương trình kiểm thử ghi và đọc dữ liệu qua pipe
  • Môi trường thí nghiệm là CPU Intel Skylake i7-8550U và Linux 5.17; vì cách triển khai nội bộ của Linux liên tục thay đổi nên các con số có thể khác tùy môi trường
  • Điểm xuất phát là một chương trình lặp lại việc đẩy bộ đệm 256KiB qua pipe bằng các system call writeread
    • write liên tục ghi cùng một bộ đệm 256KiB
    • read đọc tổng cộng 10GiB rồi in thông lượng
    • Mã nguồn nằm tại pipes-speed-test
  • Chương trình FizzBuzz được tối ưu hóa rất mạnh dùng để so sánh cho ra khoảng 36.2GiB/s khi đo bằng pv
    • FizzBuzz dùng các khối 256KiB, bằng kích thước cache L2
    • Thí nghiệm này cũng dùng cùng khối 256KiB, nhưng không tính toán nội dung đầu ra để quan sát giá trị gần với giới hạn IO của pipe
  • Kết quả triển khai đầu tiên như sau
    • ./write | ./read
    • 3.7GiB/s, bộ đệm 256KiB, 40960 lần lặp, tổng truyền 10GiB

Vì sao writeread chậm

  • Khi kiểm tra bằng perf record -gperf report, khoảng 47% thời gian phía write được tiêu tốn trong pipe_write
  • Bên trong pipe_write, copy_page_from_iter__alloc_pages chiếm tỷ trọng lớn
    • sao chép dữ liệu
    • cấp phát trang
    • lập lịch
    • chờ và đánh thức
    • lấy và nhả khóa
  • Pipe của Linux là một ring buffer chứa tham chiếu trang
    • pipe_inode_infohead, tail, và mảng pipe_buffer
    • pipe_bufferstruct page *page, offset, len
    • Kích thước trang thông thường trên x86-64 là 4KiB
    • Số slot ring buffer mặc định là 16, và ví dụ pipe 8 slot có thể chứa tối đa 32KiB
  • head biểu thị đầu ghi, còn tail biểu thị đầu đọc
    • Khi pipe đầy, write sẽ bị block
    • Khi pipe rỗng, read sẽ bị block
  • pipe_write nhìn chung diễn ra theo trình tự sau
    • Nếu pipe đã đầy thì chờ đến khi có chỗ trống
    • Nếu bộ đệm mà head hiện tại trỏ tới còn chỗ thì điền vào đó trước
    • Nếu còn slot trống và vẫn còn dữ liệu thì cấp phát trang mới, điền dữ liệu rồi cập nhật head
  • pipe_read làm ngược lại: tiêu thụ các trang, giải phóng trang đã đọc xong hoàn toàn và cập nhật tail
  • Trên đường đi này, mỗi trang bị sao chép hai lần
    • một lần từ bộ nhớ người dùng vào kernel
    • một lần nữa từ kernel trở lại bộ nhớ người dùng
  • Việc sao chép diễn ra theo đơn vị trang 4KiB, xen giữa là đồng bộ hóa và cấp phát/giải phóng trang
  • Trên cùng máy đó, tốc độ đọc RAM tuần tự một luồng khoảng 16GiB/s; xét đến độ phức tạp của đường đi pipe, việc write/read chậm hơn khoảng 4 lần cũng không có gì lạ

Giảm sao chép bằng vmsplicesplice

  • Với IO tốc độ cao, chi phí sao chép giữa bộ nhớ người dùng và bộ đệm kernel có thể trở thành nút thắt
  • Linux cung cấp các system call liên quan đến pipe để di chuyển dữ liệu không cần sao chép
    • splice: di chuyển dữ liệu giữa pipe và file descriptor
    • vmsplice: di chuyển dữ liệu từ bộ nhớ người dùng vào pipe
  • vmsplice chỉ định các bộ đệm đưa vào pipe bằng một mảng struct iovec
    • Giá trị trả về là số byte thực sự đã được đưa vào pipe
    • Do giới hạn kích thước ring buffer của pipe, toàn bộ kích thước yêu cầu có thể không vào hết trong một lần
  • vmsplice gắn bộ nhớ người dùng vào pipe mà không sao chép, nên cần cẩn thận không tái sử dụng bộ đệm trước khi phía đọc tiêu thụ xong dữ liệu đó
  • Chương trình FizzBuzz dùng double buffering cho việc này
    • Chia bộ đệm 256KiB thành hai nửa 128KiB
    • Đặt kích thước pipe thành 128KiB để tạo 32 slot theo trang 4KiB
    • Luân phiên điền dữ liệu vào hai nửa bộ đệm rồi đưa vào pipe bằng vmsplice
  • Chương trình kiểm thử thực ra không ghi lại nội dung bộ đệm, nhưng vẫn giữ cấu trúc double buffering tương tự chương trình sinh đầu ra thực tế
  • Khi thay write bằng vmsplice, thông lượng tăng lên 12.7GiB/s
    • Lượng sao chép giảm một nửa
    • Cải thiện hơn 3 lần so với write/read
  • Nếu phía đọc cũng đổi sang splice, toàn bộ sao chép bị loại bỏ và thông lượng tăng tới 32.8GiB/s
  • Tuy nhiên, cần lưu ý về độ an toàn của vmsplice
    • Nếu một trang bị splice lại, vòng đời của nó có thể kéo dài hơn
    • Việc có còn không an toàn khi không dùng SPLICE_F_GIFT hay không vẫn chưa rõ ràng
    • Muốn dùng pipe zero-copy an toàn thì vẫn cần đặc biệt cẩn trọng

iov_iter_get_pages và chi phí chuyển đổi trang

  • Sau khi áp dụng vmsplicesplice, trong perf thời gian ở đường đi vmsplice hiện lên rất rõ
    • iov_iter_get_pages
    • __mutex_lock.constprop.0
    • add_to_pipe
  • iov_iter_get_pages chuyển dải bộ nhớ ảo trong struct iovec truyền vào vmsplice thành danh sách struct page mà pipe có thể chứa
  • Tiến trình không dùng địa chỉ bộ nhớ vật lý mà dùng địa chỉ ảo
    • CPU chuyển đổi địa chỉ ảo sang địa chỉ vật lý qua bảng trang
    • Kích thước trang thông thường trên x86-64 là 4KiB
    • Bảng trang x86-64 thường được mô tả là cấu trúc cây 4 tầng, mỗi tầng 512 nhánh
  • pipe_buffer của pipe tham chiếu tới struct page
    • struct page là cấu trúc cốt lõi để kernel xử lý metadata liên quan đến các trang vật lý
    • Vì vậy vmsplice phải chuyển dải bộ nhớ ảo đầu vào thành tham chiếu trang vật lý
  • Phần lớn thời gian bên trong iov_iter_get_pages được tiêu tốn trong get_user_pages_fast
    • Với bộ đệm 128KiB và trang 4KiB, tổng cộng cần lấy 32 trang
    • Mã pipe do cách triển khai sẽ gọi với nr_pages = 16 mỗi lần và lặp lại khi cần, nhưng tổng số trang được splice vẫn là 32
  • get_user_pages_fast đi qua bảng trang bằng phần mềm để thu thập struct page, tương tự những gì CPU làm
  • Trong lúc bên gọi đang dùng trang, phải tăng reference count của struct page để trang vật lý không bị tái sử dụng
    • Sau đó phải giảm reference count bằng put_page
  • Nếu điền bộ đệm trước bằng memset khi bắt đầu, các mục bảng trang sẽ được tạo sẵn nên get_user_pages_fast có thể tránh đường chậm
    • Nếu không memset, trong ví dụ này thông lượng giảm xuống 25.0GiB/s
    • Khi dùng huge page, hiện tượng này không xuất hiện giống như vậy

Giảm chi phí xử lý trang bằng huge page

  • Ngoài trang thường 4KiB, x86-64 còn hỗ trợ huge page 2MiB và 1GiB
    • Các thí nghiệm sau đó chỉ xét huge page 2MiB
    • Trang 1GiB tương đối hiếm và được xem là quá mức cần thiết cho công việc này
  • Huge page biểu diễn cùng một dải bộ nhớ bằng ít trang hơn nên giảm chi phí quản lý
    • Khi chuyển địa chỉ ảo sang địa chỉ vật lý, số tầng bảng trang cũng giảm đi một tầng
    • Nó cũng có thể giúp giảm gánh nặng lên TLB của CPU
  • Nút thắt trực tiếp trong thí nghiệm này không phải page table walk bằng phần cứng, mà là đường đi phần mềm get_user_pages_fast trong kernel
  • Trên Linux, có thể yêu cầu dùng huge page bằng cách cấp phát bộ nhớ căn chỉnh theo 2MiB rồi gọi madvise(..., MADV_HUGEPAGE)
  • Khi áp dụng huge page, thông lượng tăng lên 51.0GiB/s
  • Lý do cải thiện không phải vì struct page trỏ thẳng tới toàn bộ một trang 2MiB
    • Mã kernel nhìn chung giả định struct page trỏ tới kích thước trang chuẩn của kiến trúc hiện tại
    • Huge page được biểu diễn bằng một head struct page và nhiều tail struct page
    • Một huge page 2MiB có thể được biểu diễn bằng tối đa 512 struct page theo chuẩn 4KiB
  • Dù vậy, sau khi tìm được entry đầu tiên thì các struct page tiếp theo có thể được tạo bằng một vòng lặp đơn giản, giúp giảm chi phí phải dò bảng trang nhiều lần
  • Trong kernel sau Linux 5.17 có struct folio để nhận diện rõ head page, giảm nhu cầu phải kiểm tra head/tail ở thời gian chạy

Giảm chi phí đồng bộ bằng busy loop

  • Sau khi áp dụng huge page, perf cho thấy thời gian trong wait_for_space__wake_up_common_lock trở nên nổi bật
    • chi phí chờ không gian có thể ghi
    • chi phí đánh thức phía đọc
  • Để tránh chi phí đồng bộ này, có thể dùng SPLICE_F_NONBLOCK với vmsplicesplice
    • Nếu không thể ghi vào pipe, lời gọi sẽ lập tức trả về EAGAIN
    • Bên gọi sẽ quay busy loop cho đến khi sẵn sàng
  • Khi áp dụng busy loop, thông lượng tăng tới 62.5GiB/s
  • Cái giá phải trả cũng rất rõ ràng
    • Trong lúc chờ vmsplice hoặc splice sẵn sàng, lõi CPU sẽ bị chiếm dụng hoàn toàn
    • Đây là cách đổi thêm mức dùng CPU để lấy độ trễ thấp hơn hoặc thông lượng cao hơn
  • Cuối cùng, benchmark tổng hợp này được cải thiện từ khoảng 3.5GiB/s lên khoảng 65GiB/s

Các chi tiết còn lại và chủ đề thực tiễn

  • Quá trình tối ưu hóa được thực hiện bằng cách xem đồng thời đầu ra perf và mã nguồn Linux
  • Các chủ đề được bàn đến còn rộng hơn chính pipe và splicing, gắn với những chủ đề lập trình hiệu năng cao nói chung
    • Vận hành không sao chép

      • ring buffer
      • phân trang và bộ nhớ ảo
      • overhead đồng bộ hóa
      • Trong mã thực tế, hai bộ đệm được cấp phát riêng để giảm tranh chấp bảng trang
      • get_user_pages tăng reference count của mục bảng trang và put_page giảm nó xuống
      • Nếu hai bộ đệm dùng các mục bảng trang khác nhau, tranh chấp khi sửa reference count sẽ giảm đi
      • Bài kiểm thử được chạy với taskset để cố định hai tiến trình ./write./read vào hai lõi CPU
      • Kho lưu trữ cũng có benchmark tổng hợp cho get_user_pages_fast
      • Có thể đo chênh lệch tốc độ khi có và không dùng huge page
      • Splicing vẫn là một khái niệm còn mơ hồ và rủi ro, và các vấn đề liên quan tiếp tục là gánh nặng cho các nhà phát triển kernel

1 bình luận

 
GN⁺ 2023-10-06
Các ý kiến trên Hacker News
  • Nếu tôi hiểu đúng, vmsplice trông gần giống một cơ chế bộ nhớ chia sẻ nhỏ giữa hai tiến trình khi được dùng đồng thời ở cả hai đầu đọc và ghi
    Tức là cả hai tiến trình đều phải cực kỳ cẩn thận về thời điểm đọc/ghi bộ đệm và cách trả lại nó sau khi dùng. Đây là cách nhanh nhưng cũng đáng sợ, và cũng đáng tiếc là cách triển khai ngây thơ mà ai cũng có thể viết lại chậm hơn 20 lần so với hiệu năng có thể đạt được

    • Nếu bạn cố viết phiên bản nhanh hơn 20 lần, đồng nghiệp có thể sẽ cho rằng bạn làm mọi thứ phức tạp quá mức và không có tinh thần làm việc nhóm
    • Tôi không nghĩ vmsplice là một cơ chế bộ nhớ chia sẻ nhỏ giữa hai tiến trình. Nó chỉ hỗ trợ zero-copy từ bộ nhớ người dùng vào pipe, còn chiều ngược lại vẫn phát sinh sao chép
      Xem chi tiết tại https://mazzo.li/posts/fast-pipes.html#fn10
  • Tôi tò mò liệu có thư viện xử lý dữ liệu nào trừu tượng hóa pipe, socket, file, bộ nhớ mà vẫn thực hiện được cả những tối ưu kiểu này không
    Tôi muốn biết trong C, C++, Rust hay các ngôn ngữ hệ thống khác có thư viện như vậy không. Vì không quen với các API như splice()vmsplice() được nhắc trong bài, nên tôi tự hỏi liệu có thư viện nào khi xây dựng ứng dụng cấp thấp có thể tự động tận dụng các tối ưu này khi có thể hay không. Tôi cũng tò mò liệu libuv, tokio, Netty có tự xử lý việc này trên Linux không; qua tìm hiểu nhanh thì có vẻ có thể là vậy

    • Có thể khác với luồng xử lý thông thường, nhưng thứ này không có tính di động, nên không đáng để trừu tượng hóa nhiều. Khả năng cao là sẽ phải tự triển khai ở từng nơi cần dùng
      Mã ở tầng cao hơn hiếm khi dùng các tính năng này, vì chúng khá chuyên biệt và phải được tối ưu riêng cho Linux. Nếu chỉ di chuyển dữ liệu trên Linux mà không cần nhìn vào nội dung, splice sẽ hữu ích. Các ứng dụng như proxy TCP/UDP chắc chắn cần đến nó, nhưng với một HTTP server thông thường thì không phù hợp lắm. Nếu bạn đang xây các ứng dụng kiểu này, bạn sẽ thường gặp các từ khóa như zero copy, và splice là một trong những kết quả đầu tiên bạn sẽ thấy
    • Có một crate cho tokio. Không tự động, nhưng có thể thú vị: https://lib.rs/crates/tokio-splice
    • Có thể nên xem Cosh. Hiện tôi đang đọc bài báo đó và suy nghĩ về nó; đây là một mô hình cung cấp trừu tượng truyền thông điệp trong khi vẫn cho phép tối ưu hóa
      Có vẻ nó không được biết đến nhiều ngoài môi trường nghiên cứu, và khả năng cao sẽ mất khá nhiều thời gian để viết một triển khai Cosh hiệu quả. Tóm lại, có ba chế độ truyền: move, share, copy. Ví dụ, truyền kiểu move sẽ chuyển hẳn dữ liệu mà bên gửi có quyền đọc/ghi sang cho bên nhận, và có thể được triển khai bằng cách ánh xạ lại bộ nhớ ảo trong bảng trang. Ngoài ra còn có thuộc tính strong/weak cho biết liệu có thể tin rằng bên gửi và bên nhận sẽ hợp tác với nhau hay phải cách ly nghiêm ngặt bằng cách ánh xạ lại quyền bộ nhớ ảo. Thành thật mà nói, tôi không biết liệu nó có thể được tối ưu đủ tốt để cạnh tranh ổn định với những thứ như pipe được tối ưu cực cao hay không; cũng có thể nó sẽ trở thành bài toán “trình biên dịch đủ thông minh”. Dù vậy, tôi nghĩ vẫn đáng để thử
      [1] https://barrelfish.org/publications/trios14-baumann-cosh.pdf
  • Thảo luận trước đó vào năm 2022: https://news.ycombinator.com/item?id=31592934

  • Một điều đáng ngạc nhiên tôi tình cờ biết được 4 năm trước là việc dùng pipe của Linux có thể tạo ra hành vi không xác định
    https://www.gibney.org/the_output_of_linux_pipes_can_be_inde...

    • Thật ra không có gì đáng ngạc nhiên. Pipe được tạo ra không thực sự truyền đi bất kỳ dữ liệu nào từ các lệnh echo
      (echo red; echo green 1>&2) | echo blue tạo ra hai subshell ở hai bên ký hiệu |. Subshell là tiến trình con của shell hiện tại, nên kế thừa các thuộc tính quan trọng như bảng file descriptor đang mở. Hai subshell chạy đồng thời, còn shell cha chỉ wait() cho đến khi mọi tiến trình con kết thúc. Nhìn chung không thể dự đoán tiến trình con nào chạy trước, và trên hệ thống đa lõi thì chúng thậm chí có thể chạy đúng nghĩa là đồng thời. Đầu ra chuẩn của subshell bên trái được nối vào đầu ghi của pipe, còn đầu vào chuẩn của subshell bên phải được nối vào đầu đọc. Nhưng echo blue không đọc input mà chỉ xuất ra, nên không có gì được đọc từ pipe. echo green >&2 gửi đầu ra chuẩn đến nơi mà lỗi chuẩn trỏ tới, chứ không phải vào pipe. Kết quả là echo greenecho blue cùng ghi trực tiếp vào cùng một file, có lẽ là terminal, tạo ra race condition, và thứ tự sẽ khác nhau tùy bên nào được lập lịch trước
    • Nghĩ sâu thêm một chút thì điều này hoàn toàn tự nhiên. Các chương trình trong pipeline chạy đồng thời
      Nếu không thì pipeline sẽ không hữu ích. Ví dụ, trong một pipeline dùng curl để tải một file tar rồi giải nén ngay, nếu phải đợi curl kết thúc rồi mới chạy tar thì sẽ nảy sinh vấn đề như phải lưu file tar trung gian lớn đó ở đâu. tar cần chạy cùng lúc với curl để giữ buffer nhỏ và chạy nhanh. Luồng điều khiển duy nhất giữa các chương trình trong pipeline là qua đầu vào chuẩn và đầu ra chuẩn. Trong ví dụ này đang ghi vào lỗi chuẩn, nên hiển nhiên nó không thuộc luồng điều khiển mang tính quyết định
    • Nếu bạn thích I/O không sao chép nhanh trên Linux thì bài này cũng đáng xem
      Nhân tiện, để tránh nhầm lẫn, “Indeterministic” là thuật ngữ triết học, còn thuật ngữ khoa học máy tính là “nondeterministic”
      0. https://blog.superpat.com/zero-copy-in-linux-with-sendfile-a...
    • Chuyện đó đáng ngạc nhiên đến vậy sao? Có lẽ sẽ dễ gỡ rối hơn nếu biết bạn kỳ vọng đầu ra nào và vì sao lại nghĩ như vậy
      Lệnh này có lẽ được cố ý viết theo kiểu kỳ lạ, và nếu là người review code thì chắc chắn sẽ nghiêng đầu thắc mắc. Có echo red nhưng nó không được truyền tới đâu cả. Cũng có thể là một trò đùa “red herring”. echo green đi ra lỗi chuẩn nên chỉ thấy trước echo blue khi nó kết thúc trước. Thứ tự chính xác phụ thuộc vào buffering đầu ra, điều này thay đổi theo time slice nào được cấp trước, cũng như theo số CPU và tải hệ thống. Vì vậy nó không xác định, nhưng theo cùng kiểu mà top cũng không xác định
    • Có trường hợp nào chuyện này gây vấn đề thực tế không? Thành thật mà nói ví dụ này trông khá gượng ép
  • Tóm lại, giả sử hai chương trình được viết tối ưu nhất có thể, tốc độ tối đa của pipe gần bằng tốc độ mà một lõi của hệ thống có thể đọc và ghi
    Về bản chất, vì kernel ánh xạ cùng các trang bộ nhớ vật lý từ đầu ra chuẩn của một chương trình sang đầu vào chuẩn của chương trình khác, thao tác này trở thành không sao chép, hoặc trong các tình huống kém tối ưu hơn thì gần với một lần sao chép nhanh. Khi biết điều này, việc dùng shell script nối hai hay nhiều công cụ bằng pipe để thực hiện các tác vụ hiệu năng rất cao vừa đáng hài lòng vừa buồn cười. Đây là một trong những công cụ hữu ích nhất trong hộp đồ nghề

    • Pipe chỉ không sao chép khi dùng splice hoặc vmsplice. Các system call riêng của Linux này khó dùng, đặc biệt là vmsplice
      Phần lớn chương trình và bộ lọc shell không dùng chúng, ngoại trừ một vài trường hợp nổi bật như pv, nên vẫn phải trả chi phí sao chép vào bộ nhớ kernel rồi lại lấy ra
    • Theo tôi biết, một giới hạn nghiêm trọng của pipe là trên Linux x86 nó chỉ có thể buffer 64KB / 16 trang. Nhìn chung khả năng cao là chậm hơn băng thông giữa lõi và bộ nhớ
    • Vì vậy nó không quan trọng như nhiều lập trình viên dùng nhiều thread vẫn nghĩ
      Tùy đặc tính tải, ứng dụng bạn đang xây có thể được triển khai gọn gàng hơn bằng pipe+process hoặc green/user-space thread. Có thể kém tiện hơn, nhưng truyền thông điệp thường vẫn tốt hơn địa ngục deadlock
    • Điều buồn cười là mọi người hoặc các đội ngũ bỏ ra nhiều tuần và rất nhiều tiền để nhận kết quả tệ hơn
    • Tôi không rành mấy phép màu hệ thống kiểu này, nhưng dữ liệu có phải đi hẳn lên bộ nhớ không? Hay cache sẽ ngăn vòng đi-về đó?
  • Bài này nói về cách làm pipe Linux nhanh hơn, nhưng các cách khác như shared memory hoặc message queue vẫn có thể nhanh hơn
    Trong các hệ thống cần chuyển nhiều dữ liệu thật nhanh, các bước bổ sung của pipe có thể làm giảm tốc độ. Khi nhiều thread chia sẻ dữ liệu, pipe cũng có thể tạo ra nhiều vấn đề hơn so với các cách khác. Vì vậy các cải tiến trong bài có thể không giúp ích nhiều trong những tình huống thực tế nơi tốc độ là yếu tố quan trọng

    • Có thể cho ví dụ không? Khi xử lý dữ liệu theo batch, chọn thứ như io_uring sẽ có lợi
      Nhưng với giao tiếp hai chiều, bên nào cũng cần thông báo rằng dữ liệu đã sẵn sàng. Có thể bạn không muốn đốt CPU để polling, và tôi không rõ những lựa chọn đó xử lý phần đồng bộ này nhanh hơn pipe như thế nào
    • Dùng thư viện message queue còn có lợi thế là ít phải bận tâm hơn về tính không tương thích giữa nhiều nền tảng
  • Tôi vốn biết về những thứ như page table, nhưng khi nối nó với phân tích hiệu năng bằng perf thì mới thấy rõ nó trung tâm đến mức nào đối với throughput

  • Pipe rất tuyệt. Thành thật mà nói, việc tiến trình khác ở CPU khác hay ở máy khác cũng không quá quan trọng
    https://github.com/nathants/s4/blob/master/examples/nyc_taxi...

  • Pipe đủ nhanh để kết hợp lặp đi lặp lại cat, sed, awk, cut, grep, uniq, jq v.v.