KHỐI XỬ LÝ SONG SONG Ở MỨC LỆNH MÁY
2.2. Kết hợp xử lý song song ở mức lệnh máy và giải pháp phần mềm về ILP ILP
2.2.1. Chương trình dịch và kỹ thuật đường ống
2.2.1.1. Sự lập lịch đường ống cơ bản và tháo bỏ vòng lặp
Để giữ cho một đường ống đầy, trạng thái song song giữa các lệnh phải được khai thác bằng cách tìm ra những trình tự của các lệnh không liên quan mà có thể được chồng chéo trong các đường ống. Để tránh việc trì hoãn đường ống, một lệnh phụ thuộc phải được tách ra từ lệnh nguồn bởi một khoảng cách trong các chu kỳ xung nhịp tương đương với độ trễ đường ống của lệnh nguồn. Khả năng của một trình biên dịch để thực hiện việc lập lịch này phụ thuộc vào lượng ILP sẵn trong chương trình và vào những thời gian chờ của các đơn vị chức năng trong đường ống. Bảng 2.2 cho thấy các thời gian chờ của đơn vị lập trình chức năng mà chúng ta sẽ giả định trong chương này, trừ khi những thời gian khác nhau được quy định rõ ràng. Chúng ta giả định chuẩn năm giai đoạn của đường ống số nguyên, để những nhánh có sự trì hoãn của 1 chu kỳ đồng hồ. Chúng ta giả định rằng các đơn vị chức năng được truyền đi đầy đủ hoặc được sao chép lại (nhiều lần như độ sâu đường ống), để một hoạt động của bất kỳ loại nào có thể được ban hành trên mọi chu kỳ xung nhịp và không có những rủi ro cấu trúc.
Trong mục nhỏ này, chúng ta tìm hiểu cách mà trình biên dịch có thể tăng số lượng ILP sẵn có bằng cách biến đổi vòng lặp. Ví dụ này minh họa cho một kỹ thuật quan trọng cũng như để thúc đẩy những biến đổi chương trình mạnh mẽ hơn.
Chúng ta sẽ dựa vào đoạn mã sau đây, có thêm một phần tử vô hướng vào một vectơ:
For (i = 1000; i> 0; i = i-1) x [i] = x [i] + s;
Chúng ta có thể thấy rằng vòng lặp này là song song do nhận thấy rằng thành phần của mỗi lần lặp là độc lập. Trước tiên, hãy xem xét việc thực hiện của vòng lặp này, nó hiển thị cách mà chúng ta có thể sử dụng song song để cải thiện hiệu suất của nó cho một đường ống MIPS với những thời gian chờ được chỉ ra ở trên.
Lệnh đưa ra kết quả Lệnh sử dụng kết quả Thời gian chờ trong chu kỳ xung đồng hồ
FP ALU thao tác FP ALU khác thao tác 3
FP ALU thao tác Lưu trữ kép 2
Nạp kép FP ALU thao tác 1
Nạp kép Lưu trữ kép 0
Bảng 2.2: Thời gian chờ của các bộ hoạt động FP được sử dụng trong chương này. Cột cuối cùng là số chu kỳ xung đồng hồ can thiệp cần thiết để tránh sự trì hoãn. Những con số này cũng tương tự như thời gian trễ trung bình chúng ta sẽ thấy trên một đơn vị FP. Thời gian chờ của một việc nạp dấu chấm động đến nơi lưu trữ là 0, do đó kết quả của việc nạp có thể được bỏ qua mà không trì hoãn việc lữu trữ. Chúng ta sẽ tiếp tục giả định độ trễ của việc nạp một số nguyên là 1 và việc thực hiện một số nguyên của ALU là 0.
Bước đầu tiên là dịch đoạn mã trên về hợp ngữ MIPS. Trong đoạn mã sau, ban đầu R1 là địa chỉ của phần tử trong mảng với địa chỉ cao nhất và F2 có giá trị vô hướng. Thanh ghi R2 được tính toán trước, để 8 (R2) là địa chỉ của phần tử cuối cùng làm việc trên đó. Mã MIPS đơn giản, không được đưa vào chương trình cho các đường ống, xem đoạn mã sau:
Loop: L.D F0,0(R1) ;F0= phần tử mảng
ADD.D F4,F0,F2 ;thêm phần tử vô hướng vào F2 S.D F4,0(R1) ;lưu trữ kết quả
DADDUI R1,R1,#-8 ;con trỏ giảm
;8 bytes (per DW) BNE R1,R2,Loop ;nhánh R1! =R2
Hãy bắt đầu bằng cách xem vòng lặp này sẽ chạy như thế nào khi nó được đưa vào chương trình trên một đường ống đơn giản cho MIPS với thời gian chờ trong bảng 2.2.
Ví dụ: Xem vòng lặp này sẽ sẽ nhìn thấy MIPS, cả khi được và không được lập lịch, bao gồm một vài trì hoãn hoặc chu kỳ xung đồng hồ rỗi. Hãy lập lịch những trì hoãn từ những phép toán dấu chấm động, nhưng hãy nhớ rằng chúng ta đang bỏ qua các nhánh đã trì hoãn.
Trả lời: Nếu không lập lịch, vòng lặp sẽ thực hiện như sau, thực hiện 9 chu kỳ:
Chu kỳ xung nhịp được đưa ra
Loop: L.D F0,0(R1) 1
stall 2
ADD.D F4,F0,F2 3
stall 4
stall 5
S.D F4,0(R1) 6
DADDUI R1,R1,#-8 7
stall 8
BNE R1,R2,Loop 9
Chúng ta có thể lập lịch vòng lặp chỉ chứa 2 trì hoãn và giảm thời gian còn 7 chu kỳ:
Loop: L.D F0,0(R1) DADDUI R1,R1,#-8 ADD.D F4,F0,F2 stall
stall
S.D F4,8(R1) BNE R1,R2,Loop
Những trì hoãn sau ADD.D được dùng bởi S.D.
Trong ví dụ trước, chúng ta hoàn thành một lần lặp và lưu trở lại vào một phần tử mảng mỗi 7 chu kỳ xung nhịp, nhưng công việc thực sự của việc thao tác trên phần tử mảng chỉ mất 3 chu kỳ (nạp, cộng và lưu trữ) trong số 7 chu kỳ xung đồng hồ. 4 chu kỳ xung đồng hồ còn lại bao gồm thủ tục bổ sung vòng lặp - DADDUI và BNE – và hai sự trì hoãn. Để loại bỏ 4 chu kỳ đồng hồ chúng ta cần phải làm nhiều phép toán hơn liên quan đến số lượng các lệnh đầu.
Đề xuất đơn giản để tăng số lượng các lệnh liên quan đến nhánh và các lệnh đầu là mở vòng lặp. Việc mở vòng lặp chỉ đơn giản là sao chép thân vòng lặp lại nhiều lần, điều chỉnh mã kết thúc vòng lặp.
Mở vòng lặp cũng có thể được sử dụng để cải thiện việc lập lịch. Bởi vì nó giúp loại bỏ nhánh, nên nó cho phép các lệnh từ những lần lặp khác nhau được đưa vào lập lịch cùng nhau. Trong trường hợp này, chúng ta có thể loại bỏ các trì hoãn sử dụng dữ liệu bằng cách tạo thêm các lệnh độc lập xung quanh thân vòng lặp. Nếu chúng ta sao chép một cách đơn giản các lệnh khi chúng ta mở vòng lặp, thì việc sử dụng kết quả của những thanh ghi giống nhau có thể ngăn cản chúng ta khỏi việc lập lịch vòng lặp có hiệu quả. Vì vậy, chúng ta sẽ sử dụng những thanh ghi khác nhau cho mỗi lần lặp, tăng số lượng thanh ghi được yêu cầu.
Ví dụ: Hãy xem việc vòng lặp được mở để có bốn bản sao của thân vòng lặp, giả sử R1 - R2 (kích thước của mảng) ban đầu là bội số của 32, nghĩa là số lần lặp là bội của 4. Hãy loại bỏ những tính toán dư thừa và không sử dụng lại bất kỳ thanh ghi nào.
Trả lời: Đây là kết quả sau việc hợp các lệnh DADDUI lại và giảm các phép toán BNE không cần thiết mà được nhân đôi trong suốt quá trình mở rộng vòng lặp.
Lưu ý rằng R2 phải được thiết lập để 32(R2) là địa chỉ bắt đầu của 4 phần tử cuối.
Loop: L.D F0,0(R1) ADD.D F4,F0,F2
S.D F4,0(R1) ;giảm DADDUI & BNE L.D F6,-8(R1)
ADD.D F8,F6,F2
S.D F8,-8(R1) ;giảm DADDUI & BNE L.D F10,-16(R1)
ADD.D F12,F10,F2
S.D F12,-16(R1) ;giảm DADDUI & BNE L.D F14,-24(R1)
ADD.D F16,F14,F2 S.D F16,-24(R1) DADDUI R1,R1,#-32 BNE R1,R2,Loop
Chúng ta đã loại bỏ ba nhánh và giảm ba bậc của R1. Các địa chỉ trong việc nạp và lưu trữ đã được hoàn trả để cho phép các lệnh DADDUI trên R1 được kết hợp. Việc tối ưu này có vẻ tầm thường, nhưng không phải vậy, nó đòi hỏi việc thay thế ký hiệu và sự rút gọn. Sự thay thế ký hiệu và sự đơn giản sẽ sắp xếp lại các biểu thức để cho phép các hằng số được thu bớt lại, cho phép một biểu thức như “((i + 1) + 1)” được viết lại thành “(i + (1 + 1))” và sau đó đơn giản thành “(i + 2)”. Chúng ta sẽ thấy những dạng tổng quát hơn của những tối ưu này việc mà dùng loại những phép tính phụ thuộc.
Nếu không lập lịch, mọi hoạt động trong vòng lặp đã mở rộng được theo sau bởi một phép tính phụ thuộc và vì thế sẽ gây ra sự trì hoãn. Vòng lặp sẽ chạy trong vòng 27 chu kỳ xung đồng hồ - mỗi LD có 1 trì hoãn, mỗi ADDD 2, DADDUI 1, cộng với 14 chu kỳ liên quan đến vấn đề lệnh - hay 6,75 chu kỳ xung đồng hồ cho mỗi 4 phần tử, nhưng nó có thể được đưa vào lập lịch để cải thiện hiệu suất đáng kể.
Mở rộng vòng lặp được thực hiện sớm trong quá trình biên dịch, để những tính toán dư thừa có thể được phơi bày và loại bỏ bởi việc tối ưu.
Trong những chương trình thực chúng ta thường không biết cận trên của vòng lặp. Giả sử đó là n và chúng ta muốn mở rộng vòng lặp để làm k bản sao thân vòng lặp. Thay vì một vòng lặp được mở rộng duy nhất, chúng ta tạo ra một cặp vòng lặp liên tiếp. Đầu tiên thực hiện (n mod k) lần và có một thân vòng lặp đó là vòng lặp ban đầu. Thứ hai là thân vòng lặp được mở rộng bao quanh bởi một vòng lặp bên
ngoài mà lặp (n/k) lần. Đối với những giá trị lớn của n, hầu hết thời gian thực hiện sẽ được dùng trong thân vòng lặp được mở rộng.
Trong ví dụ trước, việc mở rộng cải thiện việc thực hiện của vòng lặp này bằng cách loại bỏ các lệnh đầu, mặc dù nó làm tăng kích thước đoạn mã một cách đáng kể. Vòng lặp được mở rộng sẽ thực hiện như thế nào khi nó được lập lịch cho đường ống được mô tả trước đó?
Ví dụ: Hãy xem vòng lặp được mở rộng trong ví dụ trước sau khi nó được lập lịch cho đường ống với thời gian chờ xem trong bảng 2.2.
Trả lời:
Loop: L.D F0,0(R1) L.D F6,-8(R1) L.D F10,-16(R1) L.D F14,-24(R1) ADD.D F4,F0,F2 ADD.D F8,F6,F2 ADD.D F12,F10,F2 ADD.D F16,F14,F2 S.D F4,0(R1) S.D F8,-8(R1) DADDUI R1,R1,#-32 S.D F12,16(R1) S.D F16,8(R1) BNE R1,R2,Loop
Thời gian thực hiện của vòng lặp mở rộng đã giảm xuống tổng cộng 14 chu kỳ xung đồng hồ, hoặc 3,5 chu kỳ mỗi phần tử, so với 9 chu kỳ mỗi phần tử trước khi mở rộng hoặc đưa vào lập lịch và 7 chu kỳ khi lập lịch nhưng không mở rộng.
Lợi ích từ lập lịch trên vòng lặp mở rộng thậm chí lớn hơn so với trên vòng lặp gốc. Sự gia tăng này xuất hiện vì vòng lặp mở rộng thể hiện nhiều tính toán hơn mà có thể được lập lịch để tối thiểu các trì hoãn, mã ở trên không có các trì hoãn.
Việc lập lịch vòng lặp trong kiểu này cần phải nhận thấy rằng việc tải và lưu trữ phải độc lập và có thể được luân phiên.