Các thuật ngữ "ràng buộc CPU" và "ràng buộc I / O" nghĩa là gì?
Các thuật ngữ "ràng buộc CPU" và "ràng buộc I / O" nghĩa là gì?
Câu trả lời:
Nó khá trực quan:
Một chương trình bị ràng buộc bởi CPU nếu nó đi nhanh hơn nếu CPU nhanh hơn, tức là nó dành phần lớn thời gian của nó chỉ đơn giản là sử dụng CPU (thực hiện các phép tính). Một chương trình tính các chữ số mới của π thường sẽ được giới hạn bởi CPU, nó chỉ là các số giòn.
Một chương trình bị ràng buộc I / O nếu nó đi nhanh hơn nếu hệ thống con I / O nhanh hơn. Hệ thống I / O chính xác nào có nghĩa là có thể thay đổi; Tôi thường liên kết nó với đĩa, nhưng tất nhiên nói chung là kết nối mạng hoặc giao tiếp là phổ biến. Một chương trình xem qua một tệp khổng lồ cho một số dữ liệu có thể bị ràng buộc I / O, vì nút cổ chai sau đó là việc đọc dữ liệu từ đĩa (thực ra, ví dụ này có lẽ là kiểu cũ ngày nay với hàng trăm MB / s đến từ SSD).
Giới hạn CPU có nghĩa là tốc độ tiến trình xử lý bị giới hạn bởi tốc độ của CPU. Một tác vụ thực hiện các phép tính trên một tập hợp số nhỏ, ví dụ nhân các ma trận nhỏ, có khả năng bị ràng buộc CPU.
Giới hạn I / O có nghĩa là tốc độ tiến trình của quá trình bị giới hạn bởi tốc độ của hệ thống con I / O. Một tác vụ xử lý dữ liệu từ đĩa, ví dụ, đếm số lượng dòng trong một tệp có khả năng bị ràng buộc I / O.
Giới hạn bộ nhớ có nghĩa là tốc độ tiến trình xử lý bị giới hạn bởi số lượng bộ nhớ khả dụng và tốc độ truy cập bộ nhớ đó. Một tác vụ xử lý một lượng lớn dữ liệu bộ nhớ, ví dụ như nhân các ma trận lớn, có khả năng là Giới hạn bộ nhớ.
Ràng buộc bộ đệm có nghĩa là tốc độ tiến trình xử lý bị giới hạn bởi số lượng và tốc độ của bộ đệm có sẵn. Một tác vụ chỉ đơn giản là xử lý nhiều dữ liệu hơn phù hợp với bộ đệm sẽ bị ràng buộc bộ đệm.
Giới hạn I / O sẽ chậm hơn Giới hạn bộ nhớ sẽ chậm hơn Giới hạn bộ đệm sẽ chậm hơn Giới hạn CPU.
Giải pháp để bị ràng buộc I / O không nhất thiết phải có thêm Bộ nhớ. Trong một số trường hợp, thuật toán truy cập có thể được thiết kế xung quanh các giới hạn I / O, Bộ nhớ hoặc Cache. Xem các thuật toán lãng quên bộ nhớ cache .
Đa luồng
Trong câu trả lời này, tôi sẽ điều tra một trường hợp sử dụng quan trọng để phân biệt giữa công việc bị ràng buộc giữa CPU và IO: khi viết mã đa luồng.
Ví dụ ràng buộc RAM I / O: Vector Sum
Hãy xem xét một chương trình tính tổng tất cả các giá trị của một vectơ:
#define SIZE 1000000000
unsigned int is[SIZE];
unsigned int sum = 0;
size_t i = 0;
for (i = 0; i < SIZE; i++)
/* Each one of those requires a RAM access! */
sum += is[i]
Song song với việc bằng cách chia mảng bằng nhau cho mỗi lõi của bạn là hữu ích hạn chế trên các máy tính để bàn hiện đại phổ biến.
Ví dụ: trên Ubuntu 19.04 của tôi, máy tính xách tay Lenovo ThinkPad P51 có CPU: CPU Intel Core i7-7820HQ (4 lõi / 8 luồng), RAM: 2x Samsung M471A2K43BB1-CRC (2x 16GiB) Tôi nhận được kết quả như sau:
Lưu ý rằng có rất nhiều phương sai giữa chạy. Nhưng tôi không thể tăng kích thước mảng hơn nữa vì tôi đã ở mức 8GiB và tôi không có tâm trạng thống kê qua nhiều lần chạy ngày hôm nay. Tuy nhiên, điều này có vẻ giống như một lần chạy thông thường sau khi thực hiện nhiều lần chạy thủ công.
Mã điểm chuẩn:
pthread
Mã nguồn POSIX C được sử dụng trong biểu đồ.
Và đây là phiên bản C ++ tạo ra kết quả tương tự.
Tôi không biết đủ kiến trúc máy tính để giải thích đầy đủ hình dạng của đường cong, nhưng có một điều rõ ràng: tính toán không trở nên nhanh hơn 8 lần như mong đợi một cách ngây thơ do tôi sử dụng tất cả 8 luồng của mình! Vì một số lý do, 2 và 3 luồng là tối ưu, và thêm nhiều hơn chỉ khiến mọi thứ chậm hơn nhiều.
So sánh điều này với công việc bị ràng buộc bởi CPU, công việc thực sự nhanh hơn 8 lần: 'thực', 'người dùng' và 'sys' nghĩa là gì trong đầu ra của thời gian (1)?
Lý do là tất cả các bộ xử lý chia sẻ một bus bộ nhớ duy nhất liên kết với RAM:
CPU 1 --\ Bus +-----+
CPU 2 ---\__________| RAM |
... ---/ +-----+
CPU N --/
vì vậy bus bộ nhớ nhanh chóng trở thành nút cổ chai chứ không phải CPU.
Điều này xảy ra bởi vì việc thêm hai số mất một chu kỳ CPU, việc đọc bộ nhớ mất khoảng 100 chu kỳ CPU trong phần cứng 2016.
Vì vậy, công việc CPU được thực hiện trên mỗi byte dữ liệu đầu vào quá nhỏ và chúng tôi gọi đây là quá trình liên kết với IO.
Cách duy nhất để tăng tốc tính toán đó hơn nữa, là tăng tốc truy cập bộ nhớ riêng với phần cứng bộ nhớ mới, ví dụ bộ nhớ đa kênh .
Nâng cấp lên đồng hồ CPU nhanh hơn chẳng hạn sẽ không hữu ích lắm.
Những ví dụ khác
nhân ma trận được giới hạn CPU trên RAM và GPU. Đầu vào chứa:
2 * N**2
số, nhưng:
N ** 3
phép nhân được thực hiện và điều đó là đủ để song song có giá trị đối với N lớn thực tế
Đây là lý do tại sao các thư viện nhân ma trận CPU song song tồn tại như sau:
Việc sử dụng bộ nhớ cache tạo ra sự khác biệt lớn đối với tốc độ thực hiện. Xem ví dụ ví dụ so sánh GPU didactic này .
Xem thêm:
Mạng là ví dụ ràng buộc IO nguyên mẫu.
Ngay cả khi chúng tôi gửi một byte dữ liệu, vẫn cần một thời gian lớn để đến đích.
Song song với các yêu cầu mạng nhỏ như yêu cầu HTTP có thể mang lại hiệu suất rất lớn.
Nếu mạng đã hoạt động hết công suất (ví dụ: tải xuống torrent), việc song song hóa vẫn có thể tăng cải thiện độ trễ (ví dụ: bạn có thể tải trang web "cùng một lúc").
Một hoạt động ràng buộc CPU C ++ giả có một số và crunch nó rất nhiều:
Sắp xếp có vẻ là CPU dựa trên thử nghiệm sau: Đã có thuật toán song song C ++ 17 chưa? cho thấy sự cải thiện hiệu suất gấp 4 lần cho sắp xếp song song, nhưng tôi cũng muốn có một xác nhận lý thuyết hơn
Làm thế nào để tìm ra nếu bạn bị ràng buộc CPU hoặc IO
IO không phải RAM bị ràng buộc như đĩa, mạng : ps aux
, sau đó nếu CPU% / 100 < n threads
. Nếu có, bạn bị ràng buộc IO, ví dụ: chặn read
s chỉ chờ dữ liệu và bộ lập lịch đang bỏ qua quá trình đó. Sau đó sử dụng các công cụ khác như sudo iotop
để quyết định chính xác IO nào là vấn đề.
Hoặc, nếu việc thực thi nhanh chóng và bạn tham số số lượng luồng, bạn có thể thấy nó dễ dàng từ time
hiệu suất đó được cải thiện khi số lượng luồng tăng lên cho công việc bị ràng buộc của CPU: 'real', 'user' và 'sys' nghĩa là gì đầu ra của thời gian (1)?
Giới hạn RAM-IO: khó nói hơn, vì thời gian chờ RAM được bao gồm trong CPU%
các phép đo, xem thêm:
Một số tùy chọn:
GPU
GPU có một nút cổ chai IO khi bạn lần đầu tiên chuyển dữ liệu đầu vào từ RAM thông thường có thể đọc được CPU sang GPU.
Do đó, GPU chỉ có thể tốt hơn CPU cho các ứng dụng bị ràng buộc CPU.
Tuy nhiên, khi dữ liệu được truyền tới GPU, nó có thể hoạt động trên các byte đó nhanh hơn CPU, bởi vì GPU:
có nhiều nội địa hóa dữ liệu hơn hầu hết các hệ thống CPU và vì vậy dữ liệu có thể được truy cập nhanh hơn đối với một số lõi so với các hệ thống khác
khai thác song song dữ liệu và hy sinh độ trễ bằng cách bỏ qua bất kỳ dữ liệu nào chưa sẵn sàng để được vận hành ngay lập tức.
Vì GPU phải hoạt động trên dữ liệu đầu vào song song lớn, tốt hơn là chỉ nên bỏ qua dữ liệu tiếp theo có thể có sẵn thay vì chờ dữ liệu hiện tại có sẵn và chặn tất cả các hoạt động khác như CPU chủ yếu
Do đó, GPU có thể nhanh hơn CPU nếu ứng dụng của bạn:
Các lựa chọn thiết kế này ban đầu nhắm vào ứng dụng kết xuất 3D, có các bước chính như được hiển thị tại Trình tạo bóng trong OpenGL là gì và chúng ta cần chúng để làm gì?
và vì vậy chúng tôi kết luận rằng các ứng dụng đó bị ràng buộc bởi CPU.
Với sự ra đời của GPGPU có thể lập trình, chúng ta có thể quan sát một số ứng dụng GPGPU đóng vai trò là ví dụ về các hoạt động ràng buộc của CPU:
Các hoạt động xử lý hình ảnh cục bộ như bộ lọc mờ có tính song song cao.
Có thể xây dựng một bản đồ nhiệt từ dữ liệu điểm với tốc độ 60 lần mỗi giây không?
Vẽ đồ thị biểu đồ nhiệt nếu hàm vẽ đủ phức tạp.
https://www.youtube.com/watch?v=fE0P6H8eK4I "Động lực học thời gian thực: CPU so với GPU" của Jesús Martín Berlanga
Giải các phương trình vi phân từng phần như phương trình Navier Stokes của động lực học chất lỏng:
Xem thêm:
Khóa Intepreter toàn cầu CPython (GIL)
Như một trường hợp nghiên cứu nhanh, tôi muốn chỉ ra Khóa phiên dịch toàn cầu Python (GIL): Khóa trình thông dịch toàn cầu (GIL) trong CPython là gì?
Chi tiết triển khai CPython này ngăn không cho nhiều luồng Python sử dụng hiệu quả công việc gắn với CPU. Các tài liệu CPython nói:
Chi tiết triển khai CPython: Trong CPython, do Khóa phiên dịch toàn cầu, chỉ một luồng có thể thực thi mã Python cùng một lúc (ngay cả khi các thư viện hướng hiệu suất nhất định có thể khắc phục giới hạn này). Nếu bạn muốn ứng dụng của mình sử dụng tốt hơn các tài nguyên tính toán của các máy đa lõi, bạn nên sử dụng
multiprocessing
hoặcconcurrent.futures.ProcessPoolExecutor
. Tuy nhiên, luồng vẫn là một mô hình thích hợp nếu bạn muốn chạy đồng thời nhiều tác vụ ràng buộc I / O.
Do đó, ở đây chúng ta có một ví dụ trong đó nội dung giới hạn CPU không phù hợp và ràng buộc I / O.
Giới hạn CPU có nghĩa là chương trình bị tắc nghẽn bởi CPU hoặc đơn vị xử lý trung tâm, trong khi ràng buộc I / O có nghĩa là chương trình bị tắc nghẽn bởi I / O hoặc đầu vào / đầu ra, chẳng hạn như đọc hoặc ghi vào đĩa, mạng, v.v.
Nói chung, khi tối ưu hóa các chương trình máy tính, người ta cố gắng tìm ra nút thắt và loại bỏ nó. Biết rằng chương trình của bạn bị ràng buộc CPU, do đó người ta không cần tối ưu hóa một cái gì đó một cách không cần thiết.
[Và bởi "nút cổ chai", ý tôi là điều khiến chương trình của bạn chậm hơn so với những gì nó có.]
Một cách khác để diễn đạt cùng một ý tưởng:
Nếu tăng tốc CPU không tăng tốc chương trình của bạn, nó có thể bị ràng buộc I / O.
Nếu tăng tốc I / O (ví dụ: sử dụng đĩa nhanh hơn) không có ích, chương trình của bạn có thể bị ràng buộc CPU.
(Tôi đã sử dụng "có thể" vì bạn cần tính đến các tài nguyên khác. Bộ nhớ là một ví dụ.)
Khi chương trình của bạn đang chờ I / O (tức là đọc / ghi đĩa hoặc đọc / ghi mạng, v.v.), CPU có thể tự do thực hiện các tác vụ khác ngay cả khi chương trình của bạn bị dừng. Tốc độ của chương trình của bạn chủ yếu sẽ phụ thuộc vào tốc độ IO có thể xảy ra và nếu bạn muốn tăng tốc độ, bạn sẽ cần tăng tốc I / O.
Nếu chương trình của bạn đang chạy nhiều hướng dẫn chương trình và không chờ I / O, thì nó được gọi là bị ràng buộc CPU. Tăng tốc CPU sẽ giúp chương trình chạy nhanh hơn.
Trong cả hai trường hợp, chìa khóa để tăng tốc chương trình có thể không phải là tăng tốc phần cứng, mà là tối ưu hóa chương trình để giảm lượng IO hoặc CPU mà nó cần, hoặc để I / O thực hiện trong khi nó cũng tốn CPU đồ đạc.
Ràng buộc I / O đề cập đến một điều kiện trong đó thời gian cần thiết để hoàn thành một tính toán được xác định chủ yếu bởi khoảng thời gian chờ đợi cho các hoạt động đầu vào / đầu ra được hoàn thành.
Điều này ngược lại với một nhiệm vụ bị ràng buộc CPU. Tình huống này phát sinh khi tốc độ yêu cầu dữ liệu chậm hơn tốc độ được tiêu thụ hoặc nói cách khác, dành nhiều thời gian hơn để yêu cầu dữ liệu hơn là xử lý dữ liệu.
Cốt lõi của lập trình async là các đối tượng Nhiệm vụ và Nhiệm vụ, mô hình hóa các hoạt động không đồng bộ. Chúng được hỗ trợ bởi async và đang chờ từ khóa. Mô hình khá đơn giản trong hầu hết các trường hợp:
Đối với mã bị ràng buộc I / O, bạn đang chờ một thao tác trả về một Tác vụ hoặc Tác vụ bên trong một phương thức không đồng bộ.
Đối với mã giới hạn CPU, bạn chờ đợi một hoạt động được bắt đầu trên một luồng nền với phương thức Task.Run.
Từ khóa đang chờ là nơi phép màu xảy ra. Nó mang lại quyền kiểm soát cho người gọi phương thức đã thực hiện chờ đợi và cuối cùng cho phép UI phản hồi hoặc dịch vụ có thể co giãn.
Ví dụ I / O-Bound: Tải xuống dữ liệu từ dịch vụ web
private readonly HttpClient _httpClient = new HttpClient();
downloadButton.Clicked += async (o, e) =>
{
// This line will yield control to the UI as the request
// from the web service is happening.
//
// The UI thread is now free to perform other work.
var stringData = await _httpClient.GetStringAsync(URL);
DoSomethingWithData(stringData);
};
Ví dụ về giới hạn CPU: Thực hiện tính toán cho trò chơi
private DamageResult CalculateDamageDone()
{
// Code omitted:
//
// Does an expensive calculation and returns
// the result of that calculation.
}
calculateButton.Clicked += async (o, e) =>
{
// This line will yield control to the UI while CalculateDamageDone()
// performs its work. The UI thread is free to perform other work.
var damageResult = await Task.Run(() => CalculateDamageDone());
DisplayDamage(damageResult);
};
Các ví dụ ở trên cho thấy cách bạn có thể sử dụng async và chờ đợi cho công việc gắn kết I / O và CPU. Đó là chìa khóa mà bạn có thể xác định khi nào một công việc bạn cần làm là ràng buộc I / O hoặc ràng buộc CPU, bởi vì nó có thể ảnh hưởng lớn đến hiệu suất mã của bạn và có khả năng dẫn đến việc sử dụng sai các cấu trúc nhất định.
Đây là hai câu hỏi bạn nên hỏi trước khi viết bất kỳ mã nào:
Mã của bạn sẽ "chờ" một cái gì đó, chẳng hạn như dữ liệu từ cơ sở dữ liệu?
- Nếu câu trả lời của bạn là "có", thì công việc của bạn là ràng buộc I / O.
Mã của bạn sẽ được thực hiện một tính toán rất tốn kém?
- Nếu bạn trả lời "có", thì công việc của bạn bị ràng buộc bởi CPU.
Nếu công việc bạn có là ràng buộc I / O, hãy sử dụng async và chờ đợi mà không có Task.Run . Bạn không nên sử dụng Thư viện song song tác vụ. Lý do cho điều này được nêu trong bài viết Async in Depth .
Nếu công việc bạn có bị ràng buộc bởi CPU và bạn quan tâm đến khả năng đáp ứng, hãy sử dụng async và chờ đợi nhưng sinh ra công việc trên một luồng khác với Task.Run. Nếu công việc phù hợp với tính đồng thời và song song, bạn cũng nên xem xét sử dụng Thư viện song song tác vụ .
Một ứng dụng bị ràng buộc bởi CPU khi hiệu năng số học / logic / dấu phẩy động (A / L / FP) trong khi thực hiện hầu hết là gần hiệu suất cao nhất về mặt lý thuyết của bộ xử lý (dữ liệu do nhà sản xuất cung cấp và được xác định bởi các đặc tính của bộ xử lý: số lượng lõi, tần số, thanh ghi, ALU, FPU, v.v.).
Hiệu suất nhìn trộm rất khó đạt được trong các ứng dụng trong thế giới thực, vì không nói là không thể. Hầu hết các ứng dụng truy cập bộ nhớ trong các phần khác nhau của quá trình thực thi và bộ xử lý không thực hiện các hoạt động A / L / FP trong một số chu kỳ. Đây được gọi là Giới hạn Von Neumann do khoảng cách tồn tại giữa bộ nhớ và bộ xử lý.
Nếu bạn muốn ở gần hiệu năng cao nhất của CPU, một chiến lược có thể là cố gắng sử dụng lại hầu hết dữ liệu trong bộ nhớ đệm để tránh yêu cầu dữ liệu từ bộ nhớ chính. Một thuật toán khai thác tính năng này là phép nhân ma trận (nếu cả hai ma trận có thể được lưu trữ trong bộ nhớ đệm). Điều này xảy ra bởi vì nếu ma trận có kích thước n x n
thì bạn cần thực hiện các 2 n^3
thao tác chỉ sử dụng 2 n^2
số dữ liệu FP. Mặt khác, ngoài ra, ma trận là một ứng dụng ít ràng buộc CPU hơn hoặc nhiều bộ nhớ hơn so với phép nhân ma trận vì nó chỉ yêu cầu n^2
FLOP với cùng một dữ liệu.
Trong hình dưới đây, các FLOP thu được với thuật toán ngây thơ cho phép cộng ma trận và phép nhân ma trận trong Intel i5-9300H, được hiển thị:
Lưu ý rằng như mong đợi hiệu suất của phép nhân ma trận lớn hơn phép cộng ma trận. Những kết quả này có thể được sao chép bằng cách chạy test/gemm
và test/matadd
có sẵn trong kho lưu trữ này .
Tôi cũng đề nghị xem video do J. Dongarra đưa ra về hiệu ứng này.
Quá trình giới hạn I / O: - Nếu phần lớn thời gian tồn tại của một quá trình được sử dụng ở trạng thái i / o, thì quá trình đó là ai / o ràng buộc quy trình. Ví dụ: -calculator, internet explorer
Quá trình giới hạn CPU: - Nếu phần lớn thời gian của quá trình được sử dụng trong cpu, thì đó là quy trình ràng buộc cpu.