Trong nhiều trường hợp, cách tối ưu để thực hiện một số tác vụ có thể phụ thuộc vào bối cảnh mà tác vụ được thực hiện. Nếu một thói quen được viết bằng ngôn ngữ lắp ráp, thông thường sẽ không thể thay đổi chuỗi các hướng dẫn dựa trên ngữ cảnh. Ví dụ đơn giản, hãy xem xét phương pháp đơn giản sau:
inline void set_port_high(void)
{
(*((volatile unsigned char*)0x40001204) = 0xFF);
}
Một trình biên dịch cho mã ARM 32 bit, được đưa ra ở trên, có thể sẽ hiển thị nó như một cái gì đó như:
ldr r0,=0x40001204
mov r1,#0
strb r1,[r0]
[a fourth word somewhere holding the constant 0x40001204]
hoặc có lẽ
ldr r0,=0x40001000 ; Some assemblers like to round pointer loads to multiples of 4096
mov r1,#0
strb r1,[r0+0x204]
[a fourth word somewhere holding the constant 0x40001000]
Điều đó có thể được tối ưu hóa một chút trong mã được lắp ráp bằng tay, như sau:
ldr r0,=0x400011FF
strb r0,[r0+5]
[a third word somewhere holding the constant 0x400011FF]
hoặc là
mvn r0,#0xC0 ; Load with 0x3FFFFFFF
add r0,r0,#0x1200 ; Add 0x1200, yielding 0x400011FF
strb r0,[r0+5]
Cả hai cách tiếp cận được lắp ráp bằng tay sẽ cần 12 byte không gian mã thay vì 16; cái sau sẽ thay thế "tải" bằng "add", trên ARM7-TDMI thực hiện hai chu kỳ nhanh hơn. Nếu mã sẽ được thực thi trong bối cảnh r0 không biết / không quan tâm, thì các phiên bản ngôn ngữ lắp ráp sẽ có phần tốt hơn phiên bản được biên dịch. Mặt khác, giả sử trình biên dịch biết rằng một số thanh ghi [ví dụ r5] sẽ giữ một giá trị nằm trong 2047 byte của địa chỉ mong muốn 0x40001204 [ví dụ 0x40001000], và hơn nữa biết rằng một số thanh ghi khác [ví dụ r7] đang diễn ra để giữ một giá trị có bit thấp là 0xFF. Trong trường hợp đó, trình biên dịch có thể tối ưu hóa phiên bản C của mã thành đơn giản:
strb r7,[r5+0x204]
Ngắn hơn và nhanh hơn nhiều so với mã lắp ráp được tối ưu hóa bằng tay. Hơn nữa, giả sử set_port_high xảy ra trong bối cảnh:
int temp = function1();
set_port_high();
function2(temp); // Assume temp is not used after this
Hoàn toàn không hợp lý khi mã hóa cho một hệ thống nhúng. Nếu set_port_high
được viết bằng mã lắp ráp, trình biên dịch sẽ phải di chuyển r0 (giữ giá trị trả về từ function1
một nơi khác trước khi gọi mã lắp ráp, sau đó di chuyển giá trị đó trở lại r0 sau đó (vì function2
sẽ mong đợi tham số đầu tiên của nó trong r0), vì vậy mã lắp ráp "được tối ưu hóa" sẽ cần năm hướng dẫn. Ngay cả khi trình biên dịch không biết bất kỳ thanh ghi nào giữ địa chỉ hoặc giá trị cần lưu trữ, phiên bản bốn lệnh của nó (có thể điều chỉnh để sử dụng bất kỳ thanh ghi có sẵn nào - không nhất thiết là r0 và r1) sẽ đánh bại cụm "tối ưu hóa" phiên bản ngôn ngữ. Nếu trình biên dịch có địa chỉ và dữ liệu cần thiết trong r5 và r7 như được mô tả trước đó, function1
sẽ không thay đổi các thanh ghi đó và do đó nó có thể thay thếset_port_high
với một strb
lệnh đơn - bốn lệnh nhỏ hơn và nhanh hơn mã lắp ráp "tối ưu hóa bằng tay".
Lưu ý rằng mã lắp ráp được tối ưu hóa bằng tay thường có thể vượt trội hơn trình biên dịch trong trường hợp lập trình viên biết luồng chương trình chính xác, nhưng trình biên dịch tỏa sáng trong trường hợp một đoạn mã được viết trước khi bối cảnh của nó được biết hoặc khi một đoạn mã nguồn có thể được gọi từ nhiều bối cảnh [nếu set_port_high
được sử dụng ở năm mươi vị trí khác nhau trong mã, trình biên dịch có thể quyết định độc lập cho từng cách tốt nhất để mở rộng nó].
Nói chung, tôi sẽ đề xuất rằng ngôn ngữ hợp ngữ có khả năng mang lại sự cải thiện hiệu suất lớn nhất trong những trường hợp mà mỗi đoạn mã có thể được tiếp cận từ một số ngữ cảnh rất hạn chế và có thể gây bất lợi cho hiệu suất ở những nơi có một phần mã có thể được tiếp cận từ nhiều bối cảnh khác nhau. Thật thú vị (và thuận tiện) các trường hợp lắp ráp có lợi nhất cho hiệu suất thường là những trường hợp mã đơn giản và dễ đọc nhất. Những nơi mà mã ngôn ngữ lắp ráp sẽ biến thành một mớ hỗn độn thường là những nơi mà việc viết trong lắp ráp sẽ mang lại lợi ích hiệu suất nhỏ nhất.
[Lưu ý nhỏ: có một số nơi có thể sử dụng mã lắp ráp để tạo ra một mớ hỗn độn siêu tối ưu hóa; ví dụ, một đoạn mã tôi đã làm cho ARM cần lấy một từ từ RAM và thực thi một trong khoảng mười hai thói quen dựa trên sáu bit trên của giá trị (nhiều giá trị được ánh xạ tới cùng một thói quen). Tôi nghĩ rằng tôi đã tối ưu hóa mã đó thành một cái gì đó như:
ldrh r0,[r1],#2! ; Fetch with post-increment
ldrb r1,[r8,r0 asr #10]
sub pc,r8,r1,asl #2
Thanh ghi r8 luôn giữ địa chỉ của bảng công văn chính (trong vòng lặp mà mã dành 98% thời gian của nó, không có gì được sử dụng cho bất kỳ mục đích nào khác); tất cả 64 mục được đề cập đến địa chỉ trong 256 byte trước nó. Vì trong hầu hết các trường hợp, vòng lặp chính có giới hạn thời gian thực hiện cứng trong khoảng 60 chu kỳ, nên việc tìm nạp và gửi đi trong chu kỳ chín rất thuận lợi để đáp ứng mục tiêu đó. Sử dụng bảng 256 địa chỉ 32 bit sẽ nhanh hơn một chu kỳ, nhưng sẽ chiếm được 1KB RAM rất quý [flash sẽ có nhiều hơn một trạng thái chờ]. Sử dụng 64 địa chỉ 32 bit sẽ yêu cầu thêm một lệnh để che giấu một số bit từ từ đã tìm nạp và vẫn sẽ ngấu nghiến thêm 192 byte so với bảng tôi thực sự sử dụng. Sử dụng bảng bù 8 bit mang lại mã rất nhỏ gọn và nhanh chóng, nhưng không phải cái gì tôi mong đợi một trình biên dịch sẽ xuất hiện; Tôi cũng không mong đợi một trình biên dịch dành một đăng ký "toàn thời gian" để giữ địa chỉ bảng.
Đoạn mã trên được thiết kế để chạy như một hệ thống khép kín; nó có thể gọi mã C theo định kỳ, nhưng chỉ trong một số thời điểm nhất định khi phần cứng mà nó đang liên lạc có thể được đưa vào trạng thái "nhàn rỗi" một cách an toàn trong hai khoảng thời gian khoảng một phần nghìn giây mỗi 16ms.