Một hướng dẫn về mã Assembly nội dòng cho C và C++

Các khái niệm cơ bản, trung cấp và nâng cao

Đầu tiên, các tác giả mô tả cú pháp sử dụng cơ bản để mã Assembly nội dòng (inline asm) được nhúng vào các chương trình C và C++. Sau đó, các tác giả giải thích các khái niệm trung gian, như các chế độ đánh địa chỉ, danh sách các vùng ghi đè (clobbers) và các đoạn phân nhánh, cũng như nhiều chủ đề nâng cao hơn, như các vùng ghi đè bộ nhớ, thuộc tính volatile (dễ thay đổi) và các khóa sẽ được thảo luận dành cho những người muốn sử dụng mã Asembly nội dòng trong các ứng dụng đa luồng. (ND: Mã asm nội dòng là một tính năng của một số trình biên dịch cho phép mã mức thấp viết bằng Assembly được nhúng vào ngôn ngữ bậc cao như C).

Salma Elshatanoufy, Nhà phát triển phần mềm, IBM

Salma Elshatanoufy là một nhà phát triển phần mềm trong nhóm XL Compilers của IBM, ở Canada. Salma đã ở IBM, trong trong phòng thử nghiệm, được ba năm. Ngoài các trình biên dịch, cô còn quan tâm đến các ứng dụng đa luồng.



06 06 2012

Trong bài này, chúng tôi thảo luận một số kịch bản sử dụng với mã Assembly nội dòng, còn được gọi tắt là asm nội dòng (inline asm). Đối với người mới bắt đầu, chúng tôi giới thiệu cú pháp cơ bản, tham chiếu toán hạng, các ràng buộc và những cạm bẫy phổ biến mà những người mới dùng cần biết. Đối với người dùng trung cấp, chúng tôi thảo luận về danh sách các vùng ghi đè, cũng như các chủ đề phân nhánh giúp cho việc sử dụng các lệnh phân nhánh dễ dàng trong các đoạn mã Assembly nội dòng của họ trong mã nguồn C/C++. Cuối cùng, chúng tôi thảo luận các vùng ghi đè bộ nhớ và thuộc tính volatile (dễ thay đổi) dành cho những người dùng nâng cao sử dụng mã Assembly nội dòng để tối ưu hóa mã của họ. Chúng tôi kết luận bằng một ví dụ về khóa đa luồng với mã Assembly nội dòng.

Mã Assembly nội dòng cơ bản

Trong khối mã asm được thể hiện trong đoạn mã Liệt kê 1, lệnh addc được sử dụng để thêm hai biến, op1op2. Trong bất kỳ khối mã asm nào, các lệnh assembly (hợp ngữ) xuất hiện đầu tiên, tiếp theo là các đầu vào và đầu ra, được phân cách bằng một dấu hai chấm. Các lệnh assembly có thể gồm một hoặc nhiều chuỗi ký tự trong dấu nháy kép. Dấu hai chấm đầu tiên đầu tiên tách các toán hạng đầu ra, dấu hai chấm thứ hai tách các toán hạng đầu vào. Nếu có các thanh ghi bị ghi đè, chúng được chèn vào sau dấu hai chấm thứ ba. Nếu khối mã asm không có các đầu vào bị ghi đè nào, có thể bỏ qua dấu hai chấm thứ ba, như Liệt kê 2 hiển thị.

Liệt kê 1. Các mã phép toán, các đầu vào, các đầu ra và các vùng ghi đè

 int res=0; 
 int op1=20; 
 int op2=30; 
 asm ( " addc. %0,%1,%2 \n" :
          "=r"(res) : "b"(op1), "r"(op2) : "r0" );

Liệt kê 2. Không có các đầu vào bị ghi đè nào với khối mã asm, do đó, bỏ qua dấu hai chấm thứ 3

 asm ( " addc. %0,%1,%2 \n" : "=r"(res) : "b"(op1), "r"(op2) );

Lưu ý:
Danh sách các vùng ghi đè được thảo luận sau trong phần này.

Mỗi lệnh "dự kiến" các đầu vào và các đầu ra được chuyển giao theo một định dạng cụ thể. Trong ví dụ trước, lệnh addc. dự kiến các toán hạng của nó được chuyển qua các thanh ghi, do đó op1 op2 được chuyển vào khối mã asm với các ràng buộc "b""r". Để có danh sách đầy đủ tất cả các ràng buộc asm hợp pháp cho trình biên dịch C và C++ XL của IBM, hãy đọc tài liệu tham khảo về ngôn ngữ trình biên dịch.

Các ràng buộc thanh ghi về các khai báo biến

Trong một số chương trình, bạn sẽ cần buộc các biến vào các thanh ghi cụ thể. Điều này được thực hiện vào lúc khai báo biến. Ví dụ sau đây buộc biến res vào GPR0 trong suốt vòng đời của chương trình:

 int register res asm("r0")=0;

Khi kiểu biến không khớp với các kiểu thanh ghi phần cứng đích, bạn sẽ nhận được một thông báo lỗi biên dịch.

Sau khi một biến được buộc vào một thanh ghi cụ thể, không thể sử dụng thanh ghi khác để lưu giữ cùng một biến đó. Ví dụ, mã sau đây sẽ gây ra một lỗi biên dịch, biến res được liên kết với GPR0, tại thời điểm khai báo, nhưng trong khối mã asm, người dùng cố sử dụng bất kỳ thanh ghi không phải là GPR0 để chuyển res vào .

Liệt kê 3. Lỗi biên dịch khi sử dụng các ràng buộc xung đột trên một biến

 int register res asm("r0")=0; 
 asm ( " addc. %0,%1,%2 \n" :
       "=b"(res) : "b"(op1), "r"(op2) : "r0" );

Trong ví dụ ở Liệt kê 4, không có toán hạng output (đầu ra) nào cho lệnh stw do đó phần các đầu ra của mã asm còn để trống. Không có thanh ghi nào được sửa đổi, do đó, chúng là tất cả các toán hạng đầu vào và địa chỉ đích được chuyển giao qua các toán hạng đầu vào. Tuy nhiên, có một cái gì đó được sửa đổi, đó là vị trí bộ nhớ được nhằm đến. Nhưng vị trí đó không được đề cập rõ ràng trong lệnh này, vì vậy đầu ra của lệnh là ngầm ẩn chứ không rõ ràng.

Liệt kê 4. Các lệnh không có toán hạng đầu ra

 int res [] = {0,0}; 
 int a=45; 
 int *pointer = &res[0]; 
 asm ( " stw %0,%1(%2) \n" : : "r"(a), "i"(sizeof(int)),"r"(pointer));

Liệt kê 5. Các lệnh có các toán hạng được giữ lại

 int res [] = {0,0}; 
 int a=45; 
 asm ( " stw %0,%1(%2) \n" :
      "+r"(res[0]) : "r"(a), "i"(sizeof(int)),"r"(pointer));

Trong Liệt kê 5, nếu bạn muốn giữ lại giá trị ban đầu của một biến kết quả mà không nhất thiết được sửa đổi bởi khối mã asm, thì bạn cần phải sử dụng ràng buộc + (dấu cộng) để giữ lại giá trị ban đầu của biến đó, như được hiển thị với res[0].

Các địa chỉ bộ nhớ đích trong mã assembly nội dòng

Nếu một lệnh chỉ rõ hai đối số của nó dưới dạng giống như D(RA), ở đây D là một giá trị chữ và RA là một thanh ghi chung, thì điều này được thực hiện theo nghĩa là D+RA là một địa chỉ có hiệu lực. Trong trường hợp này, các ràng buộc thích hợp là "m" hay "o". Cả hai "m" và "o" đều nói đến các đối số bộ nhớ. Ràng buộc "o" được mô tả là một vị trí bộ nhớ với địa chỉ tương đối (offsettable). Tuy nhiên, trong kiến trúc POWER® của IBM®, gần như tất cả các tham khảo bộ nhớ đều yêu cầu một địa chỉ tương đối (offset), do đó, "m" và "o" là tương đương. Trong trường hợp này, bạn có thể sử dụng một ràng buộc duy nhất để nói đến hai toán hạng trong lệnh. Liệt kê 6 là một ví dụ.

Liệt kê 6. Một ràng buộc duy nhất để nói đến hai toán hạng trong lệnh

 int res [] = {0,0}; 
 int a=45; 
 asm ( " stb %1,%0 \n" : "=m"(res[1]) : "r"(a));

Dạng lệnh stb (theo tài liệu tham khảo ngôn ngữ Assembly) là: stb RS,D(RA).

Mặc dù về mặt kỹ thuật lệnh stb có ba toán hạng (một thanh ghi nguồn, một thanh ghi địa chỉ và độ dịch chuyển vị trí), nhưng mô tả asm của nó chỉ sử dụng hai ràng buộc. Ràng buộc "=m" được dùng để thông báo cho trình biên dịch là địa chỉ bộ nhớ của biến res được sử dụng cho kết quả của lệnh lưu trữ (lệnh "sync" (đồng bộ) thường được dùng cho mục đích này, nhưng cũng có sẵn các lệnh khác, như mô tả trong POWER ISA. Xem phần Tài nguyên để có liên kết). Ràng buộc "=m" cho biết rằng toán hạng là một vị trí bộ nhớ đã thay đổi. Bạn không cần biết trước địa chỉ của vị trí đích, vì trình biên dịch sẽ thực hiện tác vụ đó. Điều này cho phép trình biên dịch chọn thanh ghi đúng (ví dụ (r1 cho một biến tự động) và áp dụng tự động dịch chuyển đúng. Điều này là cần thiết, vì nói chung với một nhà lập trình asm thường không thể biết sử dụng thanh ghi địa chỉ nào và sự dịch chuyển nào. Trong những trường hợp khác, bạn cũng có thể ghi đè lên hành vi này bằng cách tính toán thủ công địa chỉ đích như trong ví dụ sau.

Liệt kê 7. Tính toán thủ công địa chỉ đích

 int res [] = {0,0}; 
 int a=45; 
 asm ( " stb %0,%1(%2) \n" : :
           "r"(a), "i"(sizeof(int)),"r"(&res));

Trong đoạn mã này, đặc tả %1(%2) mô tả một địa chỉ cơ sở và một độ dịch chuyển, ở đây %2 đại diện cho địa chỉ cơ sở và res[0]%1 đại diện cho độ dịch chuyển là sizeof(int). Kết quả là, việc lưu trữ được thực hiện tại địa chỉ có hiệu lực, res.

Lưu ý:
Với một số lệnh, không thể sử dụng GPR0 làm một địa chỉ cơ sở. Việc chỉ rõ GPR0 ra lệnh cho trình assembly (assembler) không sử dụng một thanh ghi cơ sở nữa. Để đảm bảo rằng trình biên dịch không chọn r0 cho một toán hạng, bạn có thể sử dụng ràng buộc "b" chứ không phải là "r".

Các chế độ đánh địa chỉ cho các lệnh của POWER và PowerPC

Kiểu kiến trúc của POWER IBM là RISC. Các lệnh thường hoạt động hoặc với ba đối số thanh ghi (hai thanh ghi dành cho các đối số nguồn, một thanh ghi để lưu giữ kết quả) hoặc với hai thanh ghi và một giá trị trực tiếp (một thanh ghi và một giá trị trực tiếp cho các đối số nguồn và một thanh ghi để lưu giữ kết quả). Có các trường hợp ngoại lệ cho mô hình này, nhưng hầu hết là như vậy.

Trong số các lệnh với hai thanh ghi và giá trị trực tiếp, có hai lớp con đặc biệt: các lệnh tải và các lệnh lưu trữ. Các lệnh này sử dụng giá trị trực tiếp làm độ dịch chuyển tính từ giá trị trong thanh ghi nguồn để tạo nên một "địa chỉ có hiệu lực". Giá trị độ dịch chuyển này thường là một độ dịch chuyển lên trên ngăn xếp (r1 là con trỏ ngăn xếp) hoặc là một độ dịch chuyển tới TOC (Bảng mục lục -- r2 là con trỏ TOC). TOC được sử dụng để thúc đẩy việc xây dựng mã độc lập-vị trí, cho phép tải động hiệu quả các thư viện chia sẻ trên các máy tính này.

Khi sử dụng mã asm nội dòng, bạn không phải sử dụng thanh ghi cụ thể và cũng không phải xây dựng thủ công các địa chỉ có hiệu lực. Các ràng buộc đối số được sử dụng để ra lệnh cho trình biên dịch chọn các thanh ghi hoặc xây dựng các địa chỉ có hiệu lực phù hợp với yêu cầu của các lệnh. Như vậy, nếu lệnh yêu cầu một thanh ghi chung, bạn có thể sử dụng hoặc ràng buộc "r" hoặc "b". Ràng buộc "b" đáng chú ý, vì nhiều lệnh sử dụng cách chỉ định thanh ghi số 0 một cách đặc biệt – việc chỉ định - a của thanh ghi số 0 không có nghĩa là r0 được sử dụng, mà thay vào đó một giá trị chữ là 0. Với những lệnh này, một cách khôn ngoan là sử dụng "b" để biểu thị đó là các toán hạng đầu vào để ngăn không cho trình biên dịch chọn r0. Nếu trình biên dịch chọn r0 và lệnh này coi lựa chọn đó có nghĩa là một giá trị chữ 0, lệnh này sẽ đưa ra các kết quả không đúng.

Liệt kê 8. r0 và ý nghĩa đặc biệt của nó trong lệnh stbx

 char res[8]={'a','b','c','d','e','f','g','h'}; 
 char a='y'; 
 int index=7; 
 asm (" stbx %0,%1,%2 \n" : : "r"(a), "r"(index), "r"(res) );

Ở đây, chuỗi kết quả dự kiến là abcdefgy, nhưng nếu trình biên dịch đã chọn r0 cho %1, thì kết quả sẽ không đúng là ybcdefgh. Để ngăn không cho điều này xảy ra, hãy sử dụng "b" như hiển thị trong Liệt kê 9.

Liệt kê 9. Sử dụng ràng buộc "b" để biểu thị GPR khác không

 char res[8]={'a','b','c','d','e','f','g','h'}; 
 char a='y'; 
 int index=7; 
 asm (" stbx %0,%1,%2 \n" : : "r"(a), "b"(index), "r"(res) );

Một ví dụ khác có trong khối mã asm sau đây. Mặc dù có vẻ như khối mã asm dưới đây thực hiện res = res +4, nhưng đó không phải là hành vi chức năng thực tế của đoạn mã này.

Liệt kê 10. Ý nghĩa của r0 trong toán hạng thứ hai với mã phép toán addi

 int register res 
 asm("r0")=5; 
 int b=4; 
 asm ( " addi %0,%0,%1 \n" : "+r"(res) : "i"(b) : "r0"); 
 where: addi %0(result operand), %0(input operand res),
        %3(immediate operand b)

res được buộc chặt vào r0, nên việc dịch mã asm này thành dạng assembly sẽ trở thành:
addi 0,0,4

Toán hạng thứ hai không chuyển dịch thành thanh ghi không. Thay vào đó, nó được dịch thành số không trực tiếp. Thực tế, kết quả của phép toán addi như sau:
res=0+4

Trường hợp này là riêng cho addi opcode. Nếu, thay vào đó, res bị buộc chặt vào r1, thì ta nhận được hành vi dự định ban đầu:
res=res+4


Danh sách các vùng ghi đè

Danh sách các vùng ghi đè cơ bản

Trong trường hợp khi các thanh ghi, không trực tiếp bị buộc vào các đầu vào/đầu ra, được sử dụng trong khối mã asm, người sử dụng phải chỉ rõ các thanh ghi như vậy trong danh sách các vùng ghi đè (clobbers).

Danh sách các vùng ghi đè được sử dụng để thông báo cho trình biên dịch là các thanh ghi trong danh sách này có thể bị thay đổi giá trị của chúng. Do đó, không nên sử dụng các thanh ghi này để lưu trữ dữ liệu gì khác với các lệnh mà chúng đã lưu trữ.

Trong ví dụ ở Liệt kê 11, các thanh ghi 8 và 7 được bổ sung vào danh sách các vùng ghi đè vì chúng được sử dụng trong các lệnh nhưng không bị buộc rõ rệt với bất kỳ toán hạng đầu vào/đầu ra nào. Ngoài ra, trường không của thanh ghi điều kiện được thêm vào danh sách các vùng ghi đè với lý do tương tự. Mặc dù nó không có mặt trong các toán hạng đầu vào/đầu ra, nhưng lệnh mfocrf đọc bit đó từ thanh ghi điều kiện và di chuyển giá trị này vào thanh ghi 8.

Liệt kê 11. Ví dụ về danh sách các vùng ghi đè

 asm (" addc. %0,%2,%3 \n" " mfocrf 8,0x1 \n" " andi. 7,8,0xF \n" "
   stw 7,%1 \n" : "=r"(res),"=m"(c_bit) : : "b"(a), "r"(b) : "r0","r7","r8","cr0" );
 clobbers list

Nếu, thay vào đó, lệnh mfocrf đọc từ trường 1 của thanh ghi điều kiện (cr1), thì trường đó sẽ cần được thêm vào danh sách các vùng ghi đè để thay thế. Ngoài ra, dấu chấm câu [dấu chấm] ở cuối các lệnh addc.andi. có nghĩa là các kết quả của chúng được so sánh với không và kết quả so sánh được lưu trữ trong trường 0 của thanh ghi điều kiện.

Khi các thanh ghi bị ghi đè bị bỏ sót ngoài danh sách các vùng ghi đè, các kết quả của các phép toán asm có thể không đúng. Điều này là do các thanh ghi bị ghi đè này có thể được tái sử dụng để lưu giữ các giá trị trung gian cho các phép toán khác. Trừ khi trình biên dịch phát hiện rằng các thanh ghi đó bị ghi đè, dữ liệu trung gian có thể được dùng để thực hiện các lệnh của lập trình viên, với các kết quả không chính xác. Ngoài ra, các lệnh asm của người dùng có thể ghi đè các giá trị được trình biên dịch sử dụng.

Các trường hợp ngoại lệ với danh sách các vùng ghi đè

Hầu như tất cả các thanh ghi có thể bị ghi đè, trừ các thanh ghi được liệt kê trong Bảng 1.

Bảng 1. Thanh ghi không thể bị ghi đè
Thanh ghiMô tả
r1con trỏ ngăn xếp
r2con trỏ bảng mục lục
r11con trỏ môi trường
r13con trỏ dữ liệu cục bộ của luồng chế độ 64 bit
r30thường được trình biên dịch sử dụng như một con trỏ khung ngăn xếp, con trỏ đến vùng hằng số
r31thường được trình biên dịch sử dụng như một con trỏ khung ngăn xếp, con trỏ đến vùng hằng số

Các vùng ghi đè bộ nhớ

Vùng ghi đè bộ nhớ kéo theo một hàng rào và nó cũng ảnh hưởng đến cách trình biên dịch xử lý các bí danh dữ liệu tiềm năng. Một vùng ghi đè bộ nhớ nói rằng khối mã asm thay đổi bộ nhớ nếu các lệnh asm không nói gì khác đi. Vì vậy, việc dùng các vùng ghi đè bộ nhớ sẽ là đúng khi sử dụng một lệnh xóa một dòng bộ nhớ cache chẳng hạn. Trình biên dịch sẽ giả định rằng bất kỳ dữ liệu nào có thể được gắn với vùng nhớ này qua tên bí danh đã bị lệnh đó thay đổi. Kết quả là, tất cả các dữ liệu được dùng sau khối mã asm đó sẽ được nạp lại từ bộ nhớ sau khi đoạn mã asm kết thúc công việc. Điều này tốn kém hơn nhiều so với hàng rào đơn giản, sinh ra bởi thuộc tính “volatile” (dễ thay đổi) (sẽ được thảo luận sau).

Hãy nhớ rằng, vì vùng ghi đè bộ nhớ nói rằng bất cứ thứ gì có thể được tạo bí danh, tất cả mọi thứ được sử dụng cần phải được nạp lại sau asm, bất kể mã asm có làm bất cứ điều với nó hay không. Một vùng ghi đè bộ nhớ có thể được thêm vào danh sách các vùng ghi đè đơn giản bằng cách sử dụng từ "memory" (bộ nhớ) thay cho tên thanh ghi.


Phân nhánh

Phân nhánh cơ bản

Việc phân nhánh có thể là khó khăn với mã asm nội dòng, điều này là do bạn cần biết địa chỉ của lệnh để phân nhánh tới nó, trước khi biên dịch. Mặc dù điều này là không thể, nhưng bạn có thể sử dụng các nhãn. Khi sử dụng các nhãn, địa chỉ để phân nhánh tới đó có thể được chỉ định bằng một mã định danh duy nhất, và được sử dụng như một địa chỉ nhánh đích.

Trong một tệp nguồn duy nhất, các nhãn không được lặp lại trong một khối mã asm nội dòng, cũng như trong các khối mã asm lân cận trong cùng một nguồn. Trong một chương trình đã cho, mỗi nhãn là duy nhất. Tuy nhiên, có một ngoại lệ cho quy tắc này và được áp dụng nếu bạn sử dụng phân nhánh tương đối (sẽ nói thêm sau về điều này). Với việc phân nhánh tương đối, có thể có nhiều hơn một nhãn có cùng mã định danh trong cùng một chương trình và trong cùng một khối mã asm.

Lưu ý:
Không thể sử dụng các nhãn trong mã asm để định nghĩa các macro do các xung đột vùng tên tiềm năng.

Trong ví dụ ở Liệt kê 12, nhánh xuất hiện khi bit LT, bit 0, của thanh ghi điều kiện được thiết lập. Nếu nó chưa được thiết lập, thì nhánh đó không được thực hiện.

Liệt kê 12. Ví dụ về nhánh được thực hiện khi bit LT của CR0 được thiết lập (0x80000000)

 asm ( " addic. %0,%2,%4 \n" " bc 0xC,0,here \n" " there: add
   %1,%2,%3 \n" " here: mul %0,%2,%3 \n" : "=r"(res),"=r"(res2) : 
   "r" (a),"r"(b),"r"(c) : "cr0" );

Tương tự như vậy, một nhánh sẽ xuất hiện nếu bit GT (bit 1) của thanh ghi điều kiện được thiết lập, như trong đoạn mã của Liệt kê 13.

Liệt kê 13. Ví dụ về nhánh được thực hiện khi bit GT của CR0 được thiết lập (0x40000000)

 asm ( " addic. %0,%2,%4 \n" " bc 0xC,1,here \n" " there: add
   %1,%2,%3 \n" " here: mul %0,%2,%3 \n" : "=r"(res),"=r"(res2) : 
   "r" (a),"r"(b),"r"(c) : "cr0" );

Với mã asm nội dòng, phân nhánh trong cùng một khối mã asm là hoàn toàn hợp pháp; tuy nhiên, không thể phân nhánh giữa các khối mã asm khác nhau, ngay cả khi chúng được chứa trong cùng một tệp nguồn.

Phân nhánh tương đối

Như đã thảo luận ở trên, phân nhánh tương đối cho phép bạn sử dụng lại tên của một nhãn nhiều hơn một lần trong cùng một chương trình. Tuy nhiên, nó chủ yếu được sử dụng để áp đặt vị trí của địa chỉ đích có liên quan đến lệnh phân nhánh. Đây là những ví dụ về các mã phân nhánh tương đối có thể được sử dụng:

  • F -forward (tiến lên)
  • B -backward (lùi lại)

Lưu ý:
Chúng phải được nối thêm vào đuôi các nhãn số mới đúng cú pháp.

Trong ví dụ này (Liệt kê 14), lưu ý rằng địa chỉ đích được tham chiếu là "Hereb". Trong trường hợp này, chúng tôi sử dụng nhãn của địa chỉ đích được nối thêm một hậu tố để ra lệnh nhãn này được đặt ở đâu liên quan đến chính lệnh phân nhánh. Nhãn "Here" được đặt trước lệnh phân nhánh, do đó sử dụng thêm hậu tố "b" thành "Hereb".

Liệt kê 14. Đoạn chú thích cần thiết

 asm ( " 10: lwarx %0,0,%2 \n" " cmpwi %0,0 \n" " bne-
        20f \n" " ori %0,%0,1 \n" " stwcx. %0,0,%2 \n" " bne-
        10b \n" " sync \n" " ori %1,%1,1 \n" " 20: \n" :)

Thanh ghi điều kiện

Thanh ghi điều kiện được sử dụng để nắm bắt thông tin về các kết quả của các lệnh cụ thể.

Đối với các lệnh không phải dấu phẩy động, có các hậu tố dấu chấm (.) và thiết lập CR, kết quả của phép toán được so sánh với số không.

  • Nếu kết quả là lớn hơn không, thì bit 1 của trường CR được thiết lập (0x4).
  • Nếu nó nhỏ hơn không, thì bit 0 được thiết lập (0x8).
  • Nếu kết quả bằng không, thì bit 2 được thiết lập (0x2).

Đối với tất cả các lệnh so sánh, hai giá trị được so sánh và bất kỳ trường CR nào đều có thể được thiết lập (không chỉ CR0). Bảng 2 liệt kê các bit và ý nghĩa tương ứng của chúng (có tám bộ 4 bit như vậy trong thanh ghi điều kiện, được gọi là "cr0, cr1, cr2 … cr7").

Bảng 2. Các bit của một trường CR và các ý nghĩa của các thiết lập khác nhau
BitTênMô tả
0LTRA < 0
1GTRA > 0
2EQRA = 0
3UTràn bộ nhớ với các phép toán số nguyên.

Không theo thứ tự, với các phép toán dấu phẩy động

Lưu ý:
Với các lệnh dấu phẩy động có một hậu tố dấu chấm, CR1 được đặt bằng 4 bit cao hơn trong FPSCR.


Chặn thuộc tính Volatile (dễ thay đổi)

Làm cho một khối mã asm nội dòng trở thành "volatile" như trong ví dụ này, đảm bảo rằng, khi tối ưu hóa, trình biên dịch không di chuyển bất kỳ lệnh nào ở trên hoặc dưới khối các câu lệnh asm.

 asm volatile(" addic. %0,%1,%2\n" : "=r"(res): "=r"(a),"r"(a))

Điều này có thể đặc biệt quan trọng trong trường hợp khi mã đang truy cập bộ nhớ chia sẻ. Điều này sẽ được minh họa trong phần tiếp theo về khóa đa luồng.


Khóa đa luồng

Một trong những cách sử dụng mã asm nội dòng phổ biến nhất là viết các đoạn lệnh ngắn để quản lý các khóa đa luồng. Do mô hình bộ nhớ lỏng lẻo trên kiến trúc POWER, nên việc xây dựng các khóa như vậy đòi hỏi phải sử dụng cẩn thận một cặp lệnh:

  • Một lệnh tải từ khóa và tạo ra một "vùng dữ trữ".
  • Một lệnh khác cập nhật từ khóa nếu vùng dữ trữ không bị mất trong thời gian chuyển tiếp.

Lưu ý:
Nếu vùng dự trữ đã bị mất, có thể sử dụng một vòng lặp để thử lại nhiều lần.

Liệt kê 15 cho thấy một hàm nội dòng cơ bản nhằm lấy được một khóa (có một số vấn đề với mã này, chúng tôi sẽ thảo luận sau các ví dụ này).

Liệt kê 15. Ví dụ về hàm Acquire lock được mã hóa bằng asm

 inline bool acquireLock(int *lock)
  { bool returnvalue = false; 
    int lockval; 
    asm ( "0: lwarx %0,0,%2 \n" //load lock and reserve " cmpwi 0,%0,0 \n"
     //compare the lock value to 0 " bne 1f \n" 
     //not 0 then exit function " ori %0,%0,1 \n" 
     //set the lock to 1 " stwcx. %0,0,%2 \n" 
     //try to acquire the lock " bne 0b \n"
     //reservation lost, try again " ori %1,%1,1 \n" 
     //set the return value to true "1: \n" 
     //didn't get lock, return false : "+r" (lockval), "+r" (returnvalue) : "r"(lock)
     //parameter lock is an address : "cr0" ); 
     //cmpwi, stwcx both clobber cr0 return returnvalue; 
  }

Liệt kê 16 là một ví dụ về cách sử dụng hàm nội dòng này.

Liệt kê 16. Ví dụ về cách có thể sử dụng hàm acquireLock

 if (acquireLock(lockWord))
   { //begin to use the shared region temp
                = x + 1; . . . }

Vì hàm là nội dòng, nên mã kết quả sẽ không có một cuộc gọi thực tế trong đó. Thay vào đó, nó sẽ ở trước đoạn mã sử dụng vùng chia sẻ x với các lệnh để lấy khóa.

Vấn đề đầu tiên cần nhận thấy với đoạn mã này là thiếu một lệnh đồng bộ hóa. Một trong những cải tiến hiệu năng quan trọng được mô hình bộ nhớ lỏng lẻo của kiến trúc POWER cho phép thực hiện là khả năng máy tính sắp xếp lại các tải và các kho lưu trữ để làm cho việc sử dụng các đường ống nội bộ hiệu quả hơn. Tuy nhiên, có nhiều lúc các lập trình viên cần tước bỏ quyền sắp xếp lại này ở một mức nào đó để truy cập đúng vùng lưu trữ chia sẻ. Trong trường hợp của một khóa, bạn sẽ không muốn tải dữ liệu từ vùng chia sẻ ("x" trong trường hợp trên) bị sắp xếp lại sao cho nó xảy ra trước khi lấy được khóa trên vùng đó. Vì lý do này, cần chèn vào một lệnh đồng bộ để ra lệnh cho máy tính để hạn chế việc sắp xếp lại trong trường hợp này. Lệnh sync (đồng bộ) thường được sử dụng cho mục đích này, nhưng cũng có sẵn các lệnh khác, như mô tả trong các ISA Power (xem Tài nguyên). Trong ví dụ mã ở Liệt kê 17, chúng tôi đã chèn lệnh sync vào để ngăn chặn việc sắp xếp lại các tải "x" (việc này được gọi là một "hàng rào nhập khẩu"):

Liệt kê 17. Ví dụ đồng bộ

 inline bool acquireLock(int *lock)
 { bool returnvalue = false; 
   int lockval; 
   asm ( "0: lwarx %0,0,%2 \n" 
     //load lock and reserve " cmpwi 0,%0,0 \n"
     //compare the lock value to 0 " bne 1f \n" 
     //not 0 then exit function " ori %0,%0,1 \n" 
     //set the lock to 1 " stwcx. %0,0,%2 \n" 
     //try to acquire the lock " bne 0b \n"
     //reservation lost, try again " sync \n" 
     //import barrier " ori %1,%1,1 \n" 
     //set the return value to true "1: \n" 
     //didn't get lock, return false :
                "+r" (lockval), "+r" (returnvalue) : "r"(lock) 
     //parameter lock is an address : "cr0" ); 
     //cmpwi, stwcx both clobber cr0 return returnvalue; 
 }

Trong khối mã asm đó, lệnh đồng bộ sẽ ngăn không cho bất kỳ các tải tiếp theo nào xuất hiện cho đến khi đã rõ phân nhánh trước đó diễn ra theo cách nào. Bằng cách này, biến x sẽ không được tải trừ khi nhánh này không được thực hiện và hàm acquireLock trả về true (đúng).

Vậy bây giờ chúng ta đã giải quyết xong chưa? Tiếc là chưa. Chúng ta vẫn phải lo lắng xem trình biên dịch có thể làm những gì.

Các trình biên dịch tối ưu hóa hiện đại có thể rất sốt sắng trong việc di chuyển mã -- và thậm chí loại bỏ hoàn toàn -- nếu nó cho rằng những thay đổi có thể làm cho chương trình chạy nhanh hơn mà không thay đổi ngữ nghĩa của mã. Tuy nhiên, các trình biên dịch thường không biết được những phức tạp liên quan đến việc truy cập bộ nhớ chia sẻ. Ví dụ, một trình biên dịch có thể di chuyển câu lệnh temp = x + 1; đến một vị trí cao hơn trong chương trình nếu nó xác định rằng kết quả sẽ được sắp xếp hiệu quả hơn (và nó giả định rằng nhánh "nếu" thường được thực hiện). Tất nhiên, đó sẽ là thảm hoạ theo quan điểm truy cập dữ liệu chia sẻ. Để ngăn ngừa việc di chuyển bất kỳ tải nào (hoặc bất kỳ lệnh nào) từ bên dưới mã assembly nội dòng lên một vị trí ở bên trên, bạn có thể sử dụng từ khoá "volatile" (còn được gọi là thuộc tính volatile) để sửa đổi khối mã asm, như Liệt kê 18 hiển thị.

Liệt kê 18. Ví dụ từ khóa Volatile

 inline bool acquireLock(int *lock)
 { bool returnvalue = false; 
   int lockval; 
   asm volatile ( "0: lwarx %0,0,%2 \n" 
      //load lock and reserve . . . "1: \n" 
      //didn't get lock, return false : "+r" (lockval), "+r"
                (returnvalue) : "r"(lock) 
      //parameter lock is an address : "cr0" ); 
      //cmpwi, stwcx both clobber cr0 return returnvalue; 
 }

Khi bạn làm điều này, một hàng rào nội bộ được đặt trước và sau khối mã asm để ngăn không cho các lệnh được chuyển qua nó. Và hãy nhớ rằng khối mã asm này được đặt nội dòng, do đó nó sẽ ngăn không cho việc truy cập tới x được di chuyển lên trên khóa thực hiện bằng asm.


Các vùng ghi đè bộ nhớ trong việc khóa đa luồng

Cuộc thảo luận về khóa đa luồng sẽ không đầy đủ nếu không đề cập đến các vùng ghi đè bộ nhớ. Từ khóa memory (bộ nhớ) thường được thêm vào danh sách vùng ghi đè trong các tình huống như vậy, mặc dù không luôn rõ ràng tại sao lại cần nó. Việc sử dụng bộ nhớ trong danh sách các vùng ghi đè có nghĩa rằng bộ nhớ bị thay đổi không đoán trước được bởi khối mã asm.

Tuy nhiên, các thay đổi bộ nhớ trong ví dụ khóa đã cho hoàn toàn có thể dự đoán được. Mặc dù biến lock (khóa) là một con trỏ (trỏ đến một vị trí khóa), điều này cũng không hề khó dự đoán trước hơn biểu thức "*lock" trong một chương trình C. Trong trường hợp đó, một trình biên dịch hành xử tốt nhiều khả năng sẽ liên kết biểu thức "*lock" với tất cả các biến có kiểu thích hợp và như vậy sẽ nạp lại một cách chính xác bất kỳ các biến bị ảnh hưởng nào sau khi con trỏ được sử dụng để thay đổi dữ liệu. Tuy nhiên, việc sử dụng các vùng ghi đè bộ nhớ xem ra là một thực tế phổ biến, có lẽ là do thận trọng quá mức khi làm việc với các vùng chia sẻ. Tuy vậy các lập trình viên nên biết về các hình phạt hiệu năng liên quan và các cách tiếp cận thay thế.

Khi một mã asm nội dòng có gồm thêm "memory" trong danh sách các vùng ghi đè, nó có nghĩa là bất kỳ biến nào trong chương trình có thể bị sửa đổi bởi mã asm, do đó, nó phải được nạp lại trước khi được sử dụng. Yêu cầu này gần như giáng một nhát búa tạ vào nỗ lực tối ưu hóa của trình biên dịch. Một cách tiếp cận tiềm năng nhẹ cân hơn có thể là làm cho vùng chia sẻ trở thành volatile (cùng với chính bản thân khối mã asm). Làm cho biến thành volatile có nghĩa là giá trị của biến phải được nạp lại trước khi nó được sử dụng trong bất kỳ biểu thức nào. Nếu vùng chia sẻ đang nói đến là một cấu trúc dữ liệu, ví dụ như một danh sách hoặc hàng đợi, điều này sẽ đảm bảo rằng cấu trúc đã cập nhật được nạp lại sau khi nhận được khóa. Tuy nhiên, tất cả các truy cập dữ liệu không chia sẻ có thể được hưởng phần bổ sung đầy đủ của các tối ưu hóa của trình biên dịch.

Mẹo nhỏ:

Nếu cấu trúc dữ liệu chia sẻ được truy cập bằng một con trỏ (chẳng hạn *p), hãy chắc chắn khai báo con trỏ này sao cho bạn biểu thị rằng chính đối tượng được trỏ tới là volatile, không phải là chính con trỏ đó. Ví dụ dưới đây khai báo rằng danh sách được trỏ tới bởi p là volatile:

 volatile list *p

Lời cảm ơn

Cảm ơn Ian McIntosh, Christopher Lapkowski, Jim McInnes và Jae Broadhurst. Mỗi bạn đã đóng một vai trò quan trọng trong việc xuất bản bài này.

Tài nguyên

Học tập

Lấy sản phẩm và công nghệ

Thảo luận

Bình luận

developerWorks: Đăng nhập

Các trường được đánh dấu hoa thị là bắt buộc (*).


Bạn cần một ID của IBM?
Bạn quên định danh?


Bạn quên mật khẩu?
Đổi mật khẩu

Bằng việc nhấn Gửi, bạn đã đồng ý với các điều khoản sử dụng developerWorks Điều khoản sử dụng.

 


Ở lần bạn đăng nhập đầu tiên vào trang developerWorks, một hồ sơ cá nhân của bạn được tạo ra. Thông tin trong bản hồ sơ này (tên bạn, nước/vùng lãnh thổ, và tên cơ quan) sẽ được trưng ra cho mọi người và sẽ đi cùng các nội dung mà bạn đăng, trừ khi bạn chọn việc ẩn tên cơ quan của bạn. Bạn có thể cập nhật tài khoản trên trang IBM bất cứ khi nào.

Thông tin gửi đi được đảm bảo an toàn.

Chọn tên hiển thị của bạn



Lần đầu tiên bạn đăng nhập vào trang developerWorks, một bản trích ngang được tạo ra cho bạn, bạn cần phải chọn một tên để hiển thị. Tên hiển thị của bạn sẽ đi kèm theo các nội dung mà bạn đăng tải trên developerWorks.

Tên hiển thị cần có từ 3 đến 30 ký tự. Tên xuất hiện của bạn phải là duy nhất trên trang Cộng đồng developerWorks và vì lí do an ninh nó không phải là địa chỉ email của bạn.

Các trường được đánh dấu hoa thị là bắt buộc (*).

(Tên hiển thị cần có từ 3 đến 30 ký tự)

Bằng việc nhấn Gửi, bạn đã đồng ý với các điều khoản sử dụng developerWorks Điều khoản sử dụng.

 


Thông tin gửi đi được đảm bảo an toàn.


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=70
Zone=Rational
ArticleID=819991
ArticleTitle=Một hướng dẫn về mã Assembly nội dòng cho C và C++
publish-date=06062012