วัตถุประสงค์ของบทแนะนำนี้ได้แก่
- เรียนรู้การใช้ฟังก์ชันความหมายเพื่อนำความหมายของการสอนไปใช้
- เรียนรู้ว่าฟังก์ชันความหมายเกี่ยวข้องกับคำอธิบายตัวถอดรหัส ISA อย่างไร
- เขียนฟังก์ชันความหมายสำหรับคำสั่งของ RiscV RV32I
- ทดสอบเครื่องมือจำลองขั้นสุดท้ายด้วยการเรียกใช้ "สวัสดีโลก" ขนาดเล็ก ไฟล์ปฏิบัติการ
ภาพรวมของฟังก์ชันความหมาย
ฟังก์ชันความหมายใน MPACT-Sim คือฟังก์ชันที่ใช้งานการดำเนินการ ของวิธีการเพื่อให้แสดงผลข้างเคียงในสถานะจำลอง ในลักษณะเดียวกับที่ผลข้างเคียงของคำสั่งจะปรากฏเมื่อดำเนินการใน ฮาร์ดแวร์ การนำเสนอภายในของเครื่องมือจำลองของคำสั่งที่ถอดรหัสแต่ละรายการ มี Callable ที่ใช้เรียกฟังก์ชันความหมายสำหรับ วิธีการ
ฟังก์ชันความหมายมีลายเซ็น void(Instruction *)
ซึ่งก็คือ
ฟังก์ชันที่จะนำตัวชี้ไปยังอินสแตนซ์ของคลาส Instruction
และ
แสดงผล void
คลาส Instruction
ได้รับการกำหนดไว้ใน
instruction.h
สำหรับจุดประสงค์ในการเขียนฟังก์ชันอรรถศาสตร์ที่เราสนใจเป็นพิเศษ
เวกเตอร์ของอินเทอร์เฟซตัวถูกดำเนินการต้นทางและปลายทางที่เข้าถึงโดยใช้
การเรียกเมธอด Source(int i)
และ Destination(int i)
อินเทอร์เฟซตัวถูกดำเนินการต้นทางและปลายทางจะแสดงที่ด้านล่าง
// The source operand interface provides an interface to access input values
// to instructions in a way that is agnostic about the underlying implementation
// of those values (eg., register, fifo, immediate, predicate, etc).
class SourceOperandInterface {
public:
// Methods for accessing the nth value element.
virtual bool AsBool(int index) = 0;
virtual int8_t AsInt8(int index) = 0;
virtual uint8_t AsUint8(int index) = 0;
virtual int16_t AsInt16(int index) = 0;
virtual uint16_t AsUint16(int) = 0;
virtual int32_t AsInt32(int index) = 0;
virtual uint32_t AsUint32(int index) = 0;
virtual int64_t AsInt64(int index) = 0;
virtual uint64_t AsUint64(int index) = 0;
// Return a pointer to the object instance that implements the state in
// question (or nullptr) if no such object "makes sense". This is used if
// the object requires additional manipulation - such as a fifo that needs
// to be pop'ed. If no such manipulation is required, nullptr should be
// returned.
virtual std::any GetObject() const = 0;
// Return the shape of the operand (the number of elements in each dimension).
// For instance {1} indicates a scalar quantity, whereas {128} indicates an
// 128 element vector quantity.
virtual std::vector<int> shape() const = 0;
// Return a string representation of the operand suitable for display in
// disassembly.
virtual std::string AsString() const = 0;
virtual ~SourceOperandInterface() = default;
};
// The destination operand interface is used by instruction semantic functions
// to get a writable DataBuffer associated with a piece of simulated state to
// which the new value can be written, and then used to update the value of
// the piece of state with a given latency.
class DestinationOperandInterface {
public:
virtual ~DestinationOperandInterface() = default;
// Allocates a data buffer with ownership, latency and delay line set up.
virtual DataBuffer *AllocateDataBuffer() = 0;
// Takes an existing data buffer, and initializes it for the destination
// as if AllocateDataBuffer had been called.
virtual void InitializeDataBuffer(DataBuffer *db) = 0;
// Allocates and initializes data buffer as if AllocateDataBuffer had been
// called, but also copies in the value from the current value of the
// destination.
virtual DataBuffer *CopyDataBuffer() = 0;
// Returns the latency associated with the destination operand.
virtual int latency() const = 0;
// Return a pointer to the object instance that implmements the state in
// question (or nullptr if no such object "makes sense").
virtual std::any GetObject() const = 0;
// Returns the order of the destination operand (size in each dimension).
virtual std::vector<int> shape() const = 0;
// Return a string representation of the operand suitable for display in
// disassembly.
virtual std::string AsString() const = 0;
};
วิธีพื้นฐานในการเขียนฟังก์ชันความหมายสําหรับตัวถูกดำเนินการ 3 ปกติ
เช่น คำสั่ง add
แบบ 32 บิตมีดังนี้
void MyAddFunction(Instruction *inst) {
uint32_t a = inst->Source(0)->AsUint32(0);
uint32_t b = inst->Source(1)->AsUint32(0);
uint32_t c = a + b;
DataBuffer *db = inst->Destination(0)->AllocateDataBuffer();
db->Set<uint32_t>(0, c);
db->Submit();
}
มาดูรายละเอียดการทำงานของฟังก์ชันนี้กัน 2 บรรทัดแรกของผลลัพธ์
เนื้อหาของฟังก์ชันอ่านจากตัวถูกดำเนินการต้นทาง 0 และ 1 การโทรหา AsUint32(0)
ตีความข้อมูลที่สำคัญเป็นอาร์เรย์ uint32_t
และดึงข้อมูลที่ 0
และจะเป็นเช่นนี้เสมอ ไม่ว่าการจดทะเบียนหรือมูลค่าที่สำคัญจะเป็น
มีค่าหรือไม่ ขนาด (ในองค์ประกอบ) ของตัวถูกดำเนินการต้นทางสามารถ
ที่ได้รับจากเมธอดตัวถูกดำเนินการต้นทาง shape()
ซึ่งแสดงเวกเตอร์
ที่มีจำนวนองค์ประกอบในแต่ละมิติข้อมูล เมธอดดังกล่าวแสดงผล {1}
สำหรับสเกลาร์ {16}
สำหรับเวกเตอร์องค์ประกอบ 16 และ {4, 4}
สำหรับอาร์เรย์ 4x4
uint32_t a = inst->Source(0)->AsUint32(0);
uint32_t b = inst->Source(1)->AsUint32(0);
จากนั้นระบบจะกำหนด uint32_t
ชั่วคราวที่มีชื่อว่า c
ให้กับค่า a + b
บรรทัดถัดไปอาจต้องมีคำอธิบายเพิ่มเติมเล็กน้อย
DataBuffer *db = inst->Destination(0)->AllocateDataBuffer();
DataBuffer เป็นออบเจ็กต์อ้างอิงที่นับและใช้เพื่อเก็บค่าใน
สถานะจำลอง เช่น การลงทะเบียน ค่อนข้างไม่พิมพ์เลย แต่ก็มี
ขนาดตามออบเจ็กต์ที่จัดสรรไว้ ในกรณีนี้ ขนาดดังกล่าวเท่ากับ
sizeof(uint32_t)
คำสั่งนี้จะจัดสรรขนาดบัฟเฟอร์ข้อมูลใหม่สำหรับฟิลด์
ปลายทางที่เป็นเป้าหมายของตัวถูกดำเนินการปลายทางนี้ ในกรณีนี้
การลงทะเบียนจำนวนเต็ม 32 บิต DataBuffer ยังเริ่มต้นโดยใช้
เวลาในการตอบสนองเชิงสถาปัตยกรรมสำหรับการสอน ต้องระบุในระหว่างคำแนะนำ
ถอดรหัส
บรรทัดถัดไปถือว่าอินสแตนซ์บัฟเฟอร์ข้อมูลเป็นอาร์เรย์ของ uint32_t
และ
เขียนค่าที่เก็บไว้ใน c
ไปยังองค์ประกอบที่ 0
db->Set<uint32_t>(0, c);
สุดท้าย คำสั่งสุดท้ายจะส่งบัฟเฟอร์ข้อมูลไปยังเครื่องมือจำลองที่จะใช้ เป็นค่าใหม่ของสถานะของเครื่องเป้าหมาย (ในกรณีนี้คือรีจิสเตอร์) หลัง เวลาในการตอบสนองของคำสั่งที่ตั้งไว้เมื่อถอดรหัสคำสั่งและ เติมเวกเตอร์ตัวถูกดำเนินการปลายทางแล้ว
แม้จะเป็นฟังก์ชันที่สั้นกระชับ แต่ก็มีต้นแบบอยู่เล็กน้อย โค้ดที่จะซ้ำซ้อนเมื่อดำเนินการตามคำสั่งหลังทำ นอกจากนี้อาจบดบังความหมายที่แท้จริงของการสอน อยู่ในคำสั่งซื้อ เพื่อให้เขียนฟังก์ชันความหมายสําหรับคำสั่งส่วนใหญ่ได้ง่ายขึ้น มีฟังก์ชัน helper ที่เป็นเทมเพลตหลายรายการที่กำหนดไว้ใน instruction_helpers.h ผู้ช่วยเหล่านี้จะซ่อนโค้ดสำเร็จรูปสำหรับคำสั่งที่มี 1, 2 หรือ 3 ตัวถูกดำเนินการต้นทาง และตัวถูกดำเนินการปลายทางเดี่ยว เราลองมาดูกัน ฟังก์ชันตัวโอเปอแรนด์:
// This is a templated helper function used to factor out common code in
// two operand instruction semantic functions. It reads two source operands
// and applies the function argument to them, storing the result to the
// destination operand. This version supports different types for the result and
// each of the two source operands.
template <typename Result, typename Argument1, typename Argument2>
inline void BinaryOp(Instruction *instruction,
std::function<Result(Argument1, Argument2)> operation) {
Argument1 lhs = generic::GetInstructionSource<Argument1>(instruction, 0);
Argument2 rhs = generic::GetInstructionSource<Argument2>(instruction, 1);
Result dest_value = operation(lhs, rhs);
auto *db = instruction->Destination(0)->AllocateDataBuffer();
db->SetSubmit<Result>(0, dest_value);
}
// This is a templated helper function used to factor out common code in
// two operand instruction semantic functions. It reads two source operands
// and applies the function argument to them, storing the result to the
// destination operand. This version supports different types for the result
// and the operands, but the two source operands must have the same type.
template <typename Result, typename Argument>
inline void BinaryOp(Instruction *instruction,
std::function<Result(Argument, Argument)> operation) {
Argument lhs = generic::GetInstructionSource<Argument>(instruction, 0);
Argument rhs = generic::GetInstructionSource<Argument>(instruction, 1);
Result dest_value = operation(lhs, rhs);
auto *db = instruction->Destination(0)->AllocateDataBuffer();
db->SetSubmit<Result>(0, dest_value);
}
// This is a templated helper function used to factor out common code in
// two operand instruction semantic functions. It reads two source operands
// and applies the function argument to them, storing the result to the
// destination operand. This version requires both result and source operands
// to have the same type.
template <typename Result>
inline void BinaryOp(Instruction *instruction,
std::function<Result(Result, Result)> operation) {
Result lhs = generic::GetInstructionSource<Result>(instruction, 0);
Result rhs = generic::GetInstructionSource<Result>(instruction, 1);
Result dest_value = operation(lhs, rhs);
auto *db = instruction->Destination(0)->AllocateDataBuffer();
db->SetSubmit<Result>(0, dest_value);
}
คุณจะสังเกตเห็นว่าแทนที่จะใช้ข้อความ เช่น
uint32_t a = inst->Source(0)->AsUint32(0);
ฟังก์ชันตัวช่วยจะใช้สิ่งต่อไปนี้
generic::GetInstructionSource<Argument>(instruction, 0);
GetInstructionSource
เป็นชุดฟังก์ชันตัวช่วยแบบเทมเพลตที่
ใช้เพื่อระบุวิธีการเข้าถึงที่เป็นเทมเพลตสำหรับแหล่งที่มาของคำแนะนำ
ตัวถูกดำเนินการ หากไม่มี ฟังก์ชันผู้ช่วยการสอนแต่ละอย่าง
เป็นพิเศษสำหรับแต่ละประเภท เพื่อเข้าถึงตัวถูกดำเนินการต้นทางด้วย
As<int type>()
คุณสามารถดูคำจำกัดความของเทมเพลตเหล่านี้
ฟังก์ชันใน
instruction.h
จะเห็นว่ามีการติดตั้งใช้งาน 3 แบบ ขึ้นอยู่กับว่าแหล่งที่มา
ประเภทตัวถูกดำเนินการจะเหมือนกับปลายทาง ไม่ว่าปลายทางจะเป็น
มาจากแหล่งข้อมูลต่างๆ หรือไม่ หรือแตกต่างกันทั้งหมด แต่ละเวอร์ชันของ
ฟังก์ชันจะชี้ไปยังอินสแตนซ์คำสั่ง รวมถึงคำสั่งที่เรียกใช้ได้
(รวมถึงฟังก์ชัน lambda) ซึ่งหมายความว่าตอนนี้เราสามารถเขียน add
ใหม่ได้แล้ว
ฟังก์ชันความหมายข้างต้นดังนี้
void MyAddFunction(Instruction *inst) {
generic::BinaryOp<uint32_t>(inst,
[](uint32_t a, uint32_t b) { return a + b; });
}
เมื่อคอมไพล์ด้วย bazel build -c opt
และ copts = ["-O3"]
ในบิลด์
ไฟล์นี้ควรแทรกอยู่ในบรรทัด โดยไม่มีค่าโสหุ้ย ทำให้เรามีหมายเหตุ
ความกระชับโดยไม่มีบทลงโทษด้านประสิทธิภาพ
อย่างที่กล่าวไว้ว่ามีฟังก์ชันผู้ช่วยสำหรับค่าเดี่ยว ไบนารี และสเกลาร์ และวิธีการของเวกเตอร์ที่เทียบเท่ากัน และยังเป็นเทมเพลตที่มีประโยชน์อีกด้วย การสร้างผู้ช่วยของคุณเองสำหรับการสอนที่ไม่เหมาะกับรูปแบบทั่วไป
บิลด์เริ่มต้น
หากคุณไม่ได้เปลี่ยนไดเรกทอรีเป็น riscv_semantic_functions
โปรดดำเนินการ
กันเลย จากนั้นสร้างโปรเจ็กต์ดังนี้ บิลด์นี้ควรประสบความสำเร็จ
$ bazel build :riscv32i
...<snip>...
ไม่มีไฟล์ที่สร้างขึ้นมา ดังนั้นนี่แค่ทดลองเรียกใช้เพื่อ เพื่อให้แน่ใจว่าทุกอย่างเรียบร้อยดี
เพิ่มคำสั่ง ALU ของตัวถูกดำเนินการ 3 แบบ
ตอนนี้มาเพิ่มฟังก์ชันความหมายสำหรับ ALU ทั่วไปแบบ 3 โอเปอแรนด์
วิธีทำ เปิดไฟล์ rv32i_instructions.cc
และตรวจสอบว่า
ไม่มีคำนิยามที่จะเพิ่มไปยังไฟล์ rv32i_instructions.h
ขณะที่เราอธิบายในเรื่องนี้
วิธีการที่เราจะเพิ่มมีดังต่อไปนี้
add
- บวกจำนวนเต็ม 32 บิตand
- บิตไวส์ 32 บิตและor
- บิตไวส์ 32 บิตหรือsll
- การเปลี่ยนเชิงตรรกะ 32 บิตทางซ้ายsltu
- ชุดแบบไม่ได้รับการรับรอง 32 บิตน้อยกว่าsra
- เลื่อนไปทางขวาแบบเลขคณิต 32 บิตsrl
- การเปลี่ยนด้านขวาเชิงตรรกะ 32 บิตsub
- ลบด้วยจำนวนเต็ม 32 บิตxor
- บิตไวส์ xor แบบ 32 บิต
ถ้าคุณได้ดูบทแนะนำก่อนหน้านี้แล้ว คุณอาจจำได้ว่าเราได้พัฒนา ระหว่างคำแนะนำการลงทะเบียนกับคำแนะนำการลงทะเบียนได้ทันทีใน เครื่องมือถอดรหัส เราไม่จำเป็นต้องทำแบบนั้นแล้วในเรื่องของฟังก์ชันทางอรรถศาสตร์ อินเทอร์เฟซตัวถูกดำเนินการจะอ่านค่าตัวถูกดำเนินการจากตัวถูกดำเนินการใดก็ตาม ลงทะเบียนหรือทันที โดยที่ฟังก์ชันเชิงความหมายใช้ได้โดยไม่จำเป็นต้องเข้าใจ ตัวถูกดำเนินการของแหล่งที่มาที่สำคัญคืออะไร
คำแนะนำทั้งหมดข้างต้นถือเป็นการดำเนินการบน ยกเว้น sra
ค่าที่ไม่มีเครื่องหมาย 32 บิต ในกรณีเหล่านี้ เราสามารถใช้ฟังก์ชันเทมเพลต BinaryOp
ที่เราดูก่อนหน้านี้ซึ่งมีเฉพาะอาร์กิวเมนต์ประเภทเทมเพลตเดียว กรอกข้อมูลใน
ฟังก์ชันต่างๆ ใน rv32i_instructions.cc
สอดคล้องกัน โปรดทราบว่าเฉพาะราคาต่ำสุด
บิตของตัวถูกดำเนินการที่สองไปยังคำสั่ง Shift จะใช้สำหรับการ Shift
หรือไม่เช่นนั้น การดำเนินการทั้งหมดจะอยู่ในรูปแบบ src0 op src1
:
add
:a + b
and
:a & b
or
:a | b
sll
:a << (b & 0x1f)
sltu
:(a < b) ? 1 : 0
srl
:a >> (b & 0x1f)
sub
:a - b
xor
:a ^ b
สำหรับ sra
เราจะใช้เทมเพลตอาร์กิวเมนต์ BinaryOp
3 รายการ ดูที่
อาร์กิวเมนต์ประเภทแรกคือประเภทผลลัพธ์ uint32_t
รายการที่ 2 คือ
ประเภทของตัวถูกดำเนินการต้นทางเป็น 0 ในกรณีนี้คือ int32_t
และสุดท้ายคือประเภท
ของตัวถูกดำเนินการแหล่งที่มา 1 ในกรณีนี้คือ uint32_t
ซึ่งทำให้รายละเอียดของ sra
ฟังก์ชันความหมาย:
generic::BinaryOp<uint32_t, int32_t, uint32_t>(
instruction, [](int32_t a, uint32_t b) { return a >> (b & 0x1f); });
ทำการเปลี่ยนแปลงและสร้าง คุณสามารถตรวจสอบงานกับ rv32i_instructions.cc
เพิ่มคำสั่ง ALU ของโอเปอแรนด์ 2 รายการ
คำสั่ง ALU แบบ 2 ตัวถูกดำเนินการมีเพียง 2 แบบเท่านั้น ได้แก่ lui
และ auipc
โปรไฟล์เก่า
คัดลอกตัวถูกดำเนินการต้นทางที่เปลี่ยนล่วงหน้าไปยังปลายทางโดยตรง รายการหลัง
เพิ่มที่อยู่คำสั่งไว้ด้านหน้าทันที ก่อนที่จะเขียนลงใน
ปลายทาง เข้าถึงที่อยู่วิธีการได้จากเมธอด address()
ของออบเจ็กต์ Instruction
เนื่องจากมีตัวถูกดำเนินการต้นทางเพียงตัวเดียว เราจึงใช้ BinaryOp
แทนไม่ได้
เราต้องใช้ UnaryOp
เนื่องจากเราสามารถดำเนินการกับทั้งแหล่งที่มาและ
ตัวถูกดำเนินการปลายทางเป็น uint32_t
เราสามารถใช้เทมเพลตอาร์กิวเมนต์เดี่ยวได้
เวอร์ชัน
// This is a templated helper function used to factor out common code in
// single operand instruction semantic functions. It reads one source operand
// and applies the function argument to it, storing the result to the
// destination operand. This version supports the result and argument having
// different types.
template <typename Result, typename Argument>
inline void UnaryOp(Instruction *instruction,
std::function<Result(Argument)> operation) {
Argument lhs = generic::GetInstructionSource<Argument>(instruction, 0);
Result dest_value = operation(lhs);
auto *db = instruction->Destination(0)->AllocateDataBuffer();
db->SetSubmit<Result>(0, dest_value);
}
// This is a templated helper function used to factor out common code in
// single operand instruction semantic functions. It reads one source operand
// and applies the function argument to it, storing the result to the
// destination operand. This version requires that the result and argument have
// the same type.
template <typename Result>
inline void UnaryOp(Instruction *instruction,
std::function<Result(Result)> operation) {
Result lhs = generic::GetInstructionSource<Result>(instruction, 0);
Result dest_value = operation(lhs);
auto *db = instruction->Destination(0)->AllocateDataBuffer();
db->SetSubmit<Result>(0, dest_value);
}
ส่วนเนื้อหาของฟังก์ชันความหมายสำหรับ lui
นั้นเป็นเรื่องเล็กๆ น้อยๆ ไม่มากก็น้อย
ก็แค่แสดงผลแหล่งข้อมูล ฟังก์ชันความหมายสำหรับ auipc
จะแนะนำผู้เยาว์
เนื่องจากคุณต้องเข้าถึงเมธอด address()
ใน Instruction
อินสแตนซ์ คำตอบคือเพิ่ม instruction
ในการจับภาพ lambda
ใช้ในการทำงานของ lambda ได้ แทนที่จะเป็น [](uint32_t a) { ...
}
ตามเดิม ควรเขียน lambda เป็น [instruction](uint32_t a) { ... }
ใช้ instruction
ในร่างกายแลมด้าได้แล้วตอนนี้
ทำการเปลี่ยนแปลงและสร้าง คุณสามารถตรวจสอบงานกับ rv32i_instructions.cc
เพิ่มคำแนะนำในการเปลี่ยนขั้นตอนการควบคุม
คำแนะนำการเปลี่ยนแปลงขั้นตอนการควบคุมที่คุณจำเป็นต้องใช้จะแบ่งออกเป็น เป็นคำสั่งสำหรับ Branch แบบมีเงื่อนไข (Branch ที่สั้นกว่าซึ่งดำเนินการหาก การเปรียบเทียบเป็นจริง) และวิธีการข้ามลิงก์ ซึ่งใช้เพื่อ ใช้การเรียกฟังก์ชัน (และจะลบ -and-link ออกโดยการตั้งค่าลิงก์ ลงทะเบียนเป็น 0 ทำให้การเขียนเหล่านั้นไม่ต้องดำเนินการ)
เพิ่มวิธีการสาขาแบบมีเงื่อนไข
ไม่มีฟังก์ชันตัวช่วยสำหรับคำสั่ง Branch จึงมี 2 ตัวเลือก เขียนฟังก์ชันความหมายเองตั้งแต่ต้น หรือเขียนฟังก์ชันตัวช่วยในเครื่อง เนื่องจากเราต้องดำเนินการตามคำสั่งสำหรับ Branch จำนวน 6 ข้อ ดังนั้นวิธีนี้จึงคุ้มค่า ความพยายาม ก่อนที่จะดำเนินการนั้น เรามาดูการใช้งาน Branch ฟังก์ชันอรรถศาสตร์การสอนตั้งแต่ต้น
void MyConditionalBranchGreaterEqual(Instruction *instruction) {
int32_t a = generic::GetInstructionSource<int32_t>(instruction, 0);
int32_t b = generic::GetInstructionSource<int32_t>(instruction, 1);
if (a >= b) {
uint32_t offset = generic::GetInstructionSource<uint32_t>(instruction, 2);
uint32_t target = offset + instruction->address();
DataBuffer *db = instruction->Destination(0)->AllocateDataBuffer();
db->Set<uint32_t>(0,m target);
db->Submit();
}
}
สิ่งเดียวที่แตกต่างกันในคำสั่งของ Branch คือ Branch
และประเภทข้อมูล แบบ 32 บิตแบบมีเครื่องหมายกับแบบ 32 บิตแบบมีการรับรอง
และตัวถูกดำเนินการที่มา ซึ่งหมายความว่าเราต้องมีพารามิเตอร์เทมเพลตสำหรับ
และตัวถูกดำเนินการที่มา ฟังก์ชันตัวช่วยจะต้องนำ Instruction
อินสแตนซ์และออบเจ็กต์ที่เรียกได้ เช่น std::function
ที่ส่งคืน bool
เป็นพารามิเตอร์ ฟังก์ชันตัวช่วยมีลักษณะดังนี้
template <typename OperandType>
static inline void BranchConditional(
Instruction *instruction,
std::function<bool(OperandType, OperandType)> cond) {
OperandType a = generic::GetInstructionSource<OperandType>(instruction, 0);
OperandType b = generic::GetInstructionSource<OperandType>(instruction, 1);
if (cond(a, b)) {
uint32_t offset = generic::GetInstructionSource<uint32_t>(instruction, 2);
uint32_t target = offset + instruction->address();
DataBuffer *db = instruction->Destination(0)->AllocateDataBuffer();
db->Set<uint32_t>(0, target);
db->Submit();
}
}
ตอนนี้เราสามารถเขียนฟังก์ชันความหมาย bge
(สาขาที่ลงนามมากกว่าหรือเท่ากับ) ได้แล้ว
เป็น:
void RV32IBge(Instruction *instruction) {
BranchConditional<int32_t>(instruction,
[](int32_t a, int32_t b) { return a >= b; });
}
คำแนะนำสาขาที่เหลือมีดังต่อไปนี้
- Beq - สาขาเท่ากับ
- Bgeu - สาขาที่มากกว่าหรือเท่ากับ (ไม่มีเครื่องหมาย)
- Blt - สาขาที่น้อยกว่า (ลงชื่อ)
- Bltu - สาขาที่น้อยกว่า (ไม่มีลายเซ็น)
- Bne - สาขาไม่เท่ากัน
ทำการเปลี่ยนแปลงเพื่อใช้ฟังก์ชันความหมายเหล่านี้ และ สร้างใหม่ คุณสามารถตรวจสอบงานกับ rv32i_instructions.cc
เพิ่มวิธีการข้ามและลิงก์
ไม่มีส่วนใดในการเขียนฟังก์ชันตัวช่วยสำหรับการข้ามและลิงก์ ดังนั้น เราจะต้องเขียนข้อมูลเหล่านี้ใหม่ตั้งแต่ต้น มาเริ่มกันที่ ดูที่ความหมายของวิธีการสอน
คำสั่ง jal
ใช้ค่าชดเชยจากตัวถูกดำเนินการต้นทาง 0 และเพิ่มค่าดังกล่าวลงใน
พีซีปัจจุบัน (ที่อยู่สำหรับวิธีการ) เพื่อคำนวณเป้าหมายการข้าม เป้าหมายการข้าม
จะถูกเขียนไปยังตัวถูกดำเนินการปลายทาง 0 ที่อยู่สำหรับคืนสินค้าคือที่อยู่ของ
ตามลำดับถัดไป ซึ่งคํานวณได้โดยเพิ่มเหตุการณ์ปัจจุบัน
ขนาดของวิธีการไปยังที่อยู่เดียวกัน ที่อยู่สำหรับคืนสินค้าจะเขียนถึง
ตัวถูกดำเนินการปลายทาง 1 อย่าลืมใส่เคอร์เซอร์ออบเจ็กต์คำแนะนำใน
การจับภาพแลมบ์ดา
คำสั่ง jalr
ใช้การลงทะเบียนฐานเป็นโอเปอแรนด์แหล่งที่มา 0 และออฟเซ็ต
เป็นโอเปอแรนด์แหล่งที่มา 1 แล้วนำมารวมกันเพื่อคำนวณเป้าหมายการข้าม
มิฉะนั้น จะเหมือนกับคำสั่ง jal
เขียนความหมาย 2 ภาษาตามคำอธิบายความหมายของวิธีการสอนเหล่านี้ และฟังก์ชันใหม่ๆ คุณสามารถตรวจสอบงานกับ rv32i_instructions.cc
วิธีการเพิ่มการจัดเก็บหน่วยความจำ
สิ่งที่เราต้องทำมีวิธีการ 3 อย่าง ได้แก่ ไบต์ร้านค้า
(sb
) เก็บครึ่งคำ (sh
) และคำสำหรับร้านค้า (sw
) วิธีการสำหรับร้านค้า
แตกต่างจากวิธีการที่เราปฏิบัติมาจนถึงขณะนี้ คือ
เขียนเป็นสถานะตัวประมวลผลภายใน แต่จะเขียนไปยังทรัพยากรของระบบแทน
หน่วยความจำหลัก MPACT-Sim ไม่ถือว่าหน่วยความจำเป็นโอเปอแรนด์คำสั่ง
จึงต้องเข้าถึงหน่วยความจำโดยใช้วิธีการอื่น
คำตอบคือให้เพิ่มวิธีการเข้าถึงหน่วยความจำในออบเจ็กต์ ArchState
ของ MPACT-Sim
สร้างออบเจ็กต์สถานะ RiscV ใหม่ที่มาจาก ArchState
ที่จะเพิ่มส่วนนี้ได้ ออบเจ็กต์ ArchState
จัดการทรัพยากรหลัก เช่น
รีจิสเตอร์และออบเจ็กต์สถานะอื่นๆ นอกจากนี้ยังจัดการเส้นหน่วงเวลา
เก็บบัฟเฟอร์ข้อมูลตัวถูกดำเนินการปลายทางไว้จนกว่าจะเขียนข้อมูลกลับไปยัง
ออบเจ็กต์รีจิสเตอร์ การสอนส่วนใหญ่สามารถนำมาใช้ได้โดยไม่ต้องมีความรู้เกี่ยวกับ
คลาสนี้ แต่บางส่วน เช่น การดำเนินการด้านหน่วยความจำและระบบที่เฉพาะเจาะจงอื่นๆ
คำสั่งต้องมีฟังก์ชันการทำงานอยู่ในออบเจ็กต์สถานะนี้
ลองดูฟังก์ชันความหมายสำหรับคำสั่ง fence
ที่
นำไปใช้ใน rv32i_instructions.cc
เป็นตัวอย่างแล้ว fence
คำสั่งจะคงปัญหาด้านคำสั่งไว้จนกว่าการดำเนินการด้านหน่วยความจำบางอย่างจะ
เสร็จสมบูรณ์แล้ว ซึ่งใช้เพื่อรับประกันการเรียงลำดับหน่วยความจำระหว่างคำสั่ง
การเรียกใช้ก่อนคำสั่งและการเรียกใช้ตามหลัง
// Fence.
void RV32IFence(Instruction *instruction) {
uint32_t bits = instruction->Source(0)->AsUint32(0);
int fm = (bits >> 8) & 0xf;
int predecessor = (bits >> 4) & 0xf;
int successor = bits & 0xf;
auto *state = static_cast<RiscVState *>(instruction->state());
state->Fence(instruction, fm, predecessor, successor);
}
ส่วนสำคัญของฟังก์ชันเชิงอรรถศาสตร์ของคำสั่ง fence
คือ 2 ส่วนสุดท้าย
เส้น ก่อนอื่น มีการดึงข้อมูลออบเจ็กต์สถานะโดยใช้เมธอดใน Instruction
และ downcast<>
ไปยังคลาส Derived ที่เจาะจงของ RiscV จากนั้น Fence
ของคลาส RiscVState
จะเรียกใช้การดำเนินการรั้ว
วิธีการสำหรับร้านค้าจะทำงานคล้ายกัน ก่อนอื่น ที่อยู่ที่มีผลของ
การเข้าถึงหน่วยความจำจะคำนวณจากตัวถูกดำเนินการของแหล่งที่มาคำสั่งพื้นฐานและออฟเซ็ต
ค่าที่จะจัดเก็บไว้จะดึงจากตัวถูกดำเนินการของแหล่งที่มาถัดไป ถัดไป ฟิลด์
ได้รับออบเจ็กต์สถานะ RiscV ผ่านการเรียกเมธอด state()
และ
static_cast<>
และจะเรียกเมธอดที่เหมาะสม
เมธอด RiscVState
ของออบเจ็กต์ StoreMemory
ค่อนข้างเรียบง่าย แต่มี
ผลกระทบที่เราต้องคำนึงถึง ได้แก่
void StoreMemory(const Instruction *inst, uint64_t address, DataBuffer *db);
ดังที่เห็น เมธอดจะใช้พารามิเตอร์ 3 ตัวซึ่งชี้ไปยังร้านค้า
ที่อยู่ของร้านค้า และตัวชี้ไปยัง DataBuffer
ที่มีข้อมูลสโตร์ โปรดทราบ ไม่จำเป็นต้องมีขนาด
อินสแตนซ์ DataBuffer
เองก็มีเมธอด size()
อย่างไรก็ตาม ไม่มี
ตัวถูกดำเนินการปลายทางที่เข้าถึงได้ตามคำสั่ง ซึ่งใช้เพื่อ
จัดสรรอินสแตนซ์ DataBuffer
ของขนาดที่เหมาะสม แต่เราจำเป็นต้อง
ใช้โรงงาน DataBuffer
ที่ได้รับจากเมธอด db_factory()
ใน
อินสแตนซ์ Instruction
โรงงานมีเมธอด Allocate(int size)
ซึ่งแสดงผลอินสแตนซ์ DataBuffer
ของขนาดที่ต้องการ ตัวอย่างมีดังนี้
เกี่ยวกับวิธีใช้ข้อมูลนี้เพื่อจัดสรรอินสแตนซ์ DataBuffer
สำหรับร้านค้าแบบครึ่งคำ
(โปรดทราบว่า auto
เป็นฟีเจอร์ C++ ที่อนุมานประเภทจากมือขวา
ของงาน):
auto *state = down_cast<RiscVState *>(instruction->state());
auto *db = state->db_factory()->Allocate(sizeof(uint16_t));
เมื่อเราได้รับอินสแตนซ์ DataBuffer
แล้ว เราสามารถเขียนไปยังอินสแตนซ์ได้ตามปกติ:
db->Set<uint16_t>(0, value);
จากนั้นส่งไปยังอินเทอร์เฟซของที่เก็บหน่วยความจำ ดังนี้
state->StoreMemory(instruction, address, db);
เรายังดำเนินการไม่เสร็จ อินสแตนซ์ DataBuffer
มีการนับข้อมูลอ้างอิง ช่วงเวลานี้
เป็นที่เข้าใจและจัดการโดยเมธอด Submit
ตามปกติ ดังนั้นเพื่อให้การตั้งค่า
กรณีการใช้งานที่พบบ่อยที่สุด แต่ StoreMemory
จะไม่
เขียนไว้แบบนั้น ซึ่งจะIncRef
อินสแตนซ์ DataBuffer
ขณะดำเนินการ
แล้วตามด้วย DecRef
เมื่อเสร็จสิ้น แต่ถ้าฟังก์ชันความหมายไม่
DecRef
เป็นข้อมูลอ้างอิงของตัวเอง และจะไม่มีการอ้างสิทธิ์ซ้ำ ดังนั้น บรรทัดสุดท้ายจะมี
เป็น:
db->DecRef();
ฟังก์ชันร้านค้ามีอยู่ 3 อย่าง และสิ่งเดียวที่แตกต่างกันคือขนาดของ
การเข้าถึงหน่วยความจำ นี่ดูเหมือนจะเป็นโอกาสที่ดีสำหรับคนในพื้นที่
ฟังก์ชันตัวช่วยเทมเพลต สิ่งเดียวที่แตกต่างกันในหน้าต่างๆ ของ Store คือ
ประเภทของค่าร้านค้า ดังนั้นเทมเพลตจะต้องมีค่านั้นเป็นอาร์กิวเมนต์
นอกจากนี้ คุณต้องส่งเฉพาะอินสแตนซ์ Instruction
ใน
template <typename ValueType>
inline void StoreValue(Instruction *instruction) {
auto base = generic::GetInstructionSource<uint32_t>(instruction, 0);
auto offset = generic::GetInstructionSource<uint32_t>(instruction, 1);
uint32_t address = base + offset;
auto value = generic::GetInstructionSource<ValueType>(instruction, 2);
auto *state = down_cast<RiscVState *>(instruction->state());
auto *db = state->db_factory()->Allocate(sizeof(ValueType));
db->Set<ValueType>(0, value);
state->StoreMemory(instruction, address, db);
db->DecRef();
}
ไปทำฟังก์ชันอรรถศาสตร์ของ Store ให้เสร็จแล้วสร้างให้เสร็จเลย คุณสามารถตรวจสอบ แข่งกับ rv32i_instructions.cc
เพิ่มวิธีการโหลดหน่วยความจำ
คำแนะนำการโหลดที่จำเป็นต้องดำเนินการมีดังต่อไปนี้
lb
- โหลดไบต์ แล้วขยายลงในคำlbu
- โหลดไบต์ที่ไม่มีเครื่องหมาย ขยายเป็นศูนย์ในคำlh
- โหลดคำครึ่งคำ แล้วขยายคำนั้นลงในคำlhu
- โหลดคำที่ไม่มีเครื่องหมายครึ่งคำ ขยายเป็นศูนย์ในคำlw
- โหลดคำ
คำสั่งการโหลดเป็นคำสั่งที่ซับซ้อนที่สุดที่เราต้องใช้ในการจำลองแบบ
บทแนะนำนี้ ซึ่งคล้ายกับวิธีการของร้าน กล่าวคือ
เข้าถึงออบเจ็กต์ RiscVState
แต่เพิ่มความซับซ้อนในการโหลดแต่ละครั้ง
คำสั่งจะแบ่งเป็น 2 ฟังก์ชันทางอรรถศาสตร์ที่แยกจากกัน ตัวเลือกแรกคือ
คล้ายกับวิธีการของร้านค้า เพราะมีการคำนวณที่อยู่อย่างมีประสิทธิภาพ
และเริ่มต้นการเข้าถึงหน่วยความจำ รายการที่ 2 จะถูกเรียกใช้เมื่อหน่วยความจำ
เสร็จสมบูรณ์ และเขียนข้อมูลหน่วยความจำไปยังปลายทางการลงทะเบียน
ตัวถูกดำเนินการ
มาเริ่มต้นด้วยการประกาศเมธอด LoadMemory
ใน RiscVState
void LoadMemory(const Instruction *inst, uint64_t address, DataBuffer *db,
Instruction *child_inst, ReferenceCount *context);
LoadMemory
ใช้เวลาเพิ่มเติมอีก 2 วิธีเมื่อเทียบกับเมธอด StoreMemory
พารามิเตอร์: ตัวชี้ไปยังอินสแตนซ์ Instruction
และตัวชี้ไปยังอินสแตนซ์
การอ้างอิงนับ context
ออบเจ็กต์ วิธีแรกคือการสอนสำหรับเด็กที่
ใช้การเขียนย้อนกลับการลงทะเบียน (ตามที่อธิบายไว้ในบทแนะนำตัวถอดรหัส ISA) ทั้งนี้
มีการเข้าถึงโดยใช้เมธอด child()
ในอินสแตนซ์ Instruction
ปัจจุบัน
รายการหลังเป็นตัวชี้ไปยังอินสแตนซ์ของชั้นเรียนที่มาจาก
ReferenceCount
ที่ในกรณีนี้จะจัดเก็บอินสแตนซ์ DataBuffer
ที่จะ
เก็บข้อมูลที่โหลดไว้ ออบเจ็กต์บริบทมีให้ใช้งานผ่านทาง
context()
ในออบเจ็กต์ Instruction
(แต่สำหรับวิธีการส่วนใหญ่
ตั้งค่าเป็น nullptr
)
ออบเจ็กต์บริบทสำหรับการโหลดหน่วยความจำ RiscV ได้รับการกำหนดเป็นโครงสร้างต่อไปนี้
// A simple load context class for convenience.
struct LoadContext : public generic::ReferenceCount {
explicit LoadContext(DataBuffer *vdb) : value_db(vdb) {}
~LoadContext() override {
if (value_db != nullptr) value_db->DecRef();
}
// Override the base class method so that the data buffer can be DecRef'ed
// when the context object is recycled.
void OnRefCountIsZero() override {
if (value_db != nullptr) value_db->DecRef();
value_db = nullptr;
// Call the base class method.
generic::ReferenceCount::OnRefCountIsZero();
}
// Data buffers for the value loaded from memory (byte, half, word, etc.).
DataBuffer *value_db = nullptr;
};
คำแนะนำการโหลดจะเหมือนกันทั้งหมด ยกเว้นขนาดข้อมูล (ไบต์ แบบครึ่งคำ และคำ) และระบุว่าค่าที่โหลดเป็นแบบขยายเครื่องหมายไหม ปัจจัยหลังจะพิจารณาเฉพาะการสอนย่อย มาสร้างเทมเพลต สำหรับคำแนะนำการโหลดหลัก ซึ่งจะคล้ายกับ คำสั่งของ Store เว้นแต่จะไม่เข้าถึงตัวถูกดำเนินการต้นทางเพื่อรับค่า จากนั้นโมเดลจะสร้างออบเจ็กต์บริบท
template <typename ValueType>
inline void LoadValue(Instruction *instruction) {
auto base = generic::GetInstructionSource<uint32_t>(instruction, 0);
auto offset = generic::GetInstructionSource<uint32_t>(instruction, 1);
uint32_t address = base + offset;
auto *state = down_cast<RiscVState *>(instruction->state());
auto *db = state->db_factory()->Allocate(sizeof(ValueType));
db->set_latency(0);
auto *context = new riscv::LoadContext(db);
state->LoadMemory(instruction, address, db, instruction->child(), context);
context->DecRef();
}
จะเห็นได้ว่าความแตกต่างหลักๆ คืออินสแตนซ์ DataBuffer
ที่จัดสรร
ส่งผ่านไปยังการเรียก LoadMemory
เป็นพารามิเตอร์ รวมทั้งจัดเก็บไว้ใน
ออบเจ็กต์ LoadContext
รายการ
ฟังก์ชันความหมายของการสอนสำหรับเด็กนั้นคล้ายกันมาก อย่างแรก ฟิลด์
ได้ LoadContext
จากการเรียกใช้เมธอด Instruction
context()
และ
แคสต์ไปยัง LoadContext *
แล้ว ประการที่สอง ค่า (ตามข้อมูล
type) จะอ่านจากอินสแตนซ์ DataBuffer
ของข้อมูลโหลด ประเภทที่ 3
อินสแตนซ์ DataBuffer
ได้รับการจัดสรรจากตัวถูกดำเนินการปลายทาง สุดท้าย
ระบบจะเขียนค่าที่โหลดไปยังอินสแตนซ์ DataBuffer
ใหม่ และ Submit
ขอย้ำอีกครั้งว่าควรใช้ฟังก์ชันตัวช่วยที่เป็นเทมเพลต
template <typename ValueType>
inline void LoadValueChild(Instruction *instruction) {
auto *context = down_cast<riscv::LoadContext *>(instruction->context());
uint32_t value = static_cast<uint32_t>(context->value_db->Get<ValueType>(0));
auto *db = instruction->Destination(0)->AllocateDataBuffer();
db->Set<uint32_t>(0, value);
db->Submit();
}
ดำเนินการต่อและใช้ฟังก์ชันผู้ช่วยตัวสุดท้ายและฟังก์ชันความหมาย ชำระเงิน ประเภทข้อมูลที่คุณใช้ในเทมเพลตสำหรับฟังก์ชันตัวช่วยแต่ละรายการ และสอดคล้องกับขนาดและลักษณะที่มีการรับรอง/ไม่มีการลงชื่อของการโหลด วิธีการ
คุณสามารถตรวจสอบงานกับ rv32i_instructions.cc
สร้างและเรียกใช้เครื่องมือจำลองขั้นสุดท้าย
หลังจากทำงานยากๆ ไปหมดแล้ว เราจึงจะสามารถสร้างเครื่องมือจำลองขั้นสุดท้ายได้
ไลบรารี C++ ระดับบนสุดที่รวบรวมงานทั้งหมดในบทแนะนำเหล่านี้เข้าด้วยกัน
อยู่ใน other/
คุณไม่จำเป็นต้องดูรหัสนั้นให้ยากเกินไป พ
จะเข้าชมหัวข้อนั้นในบทแนะนำขั้นสูงในอนาคต
เปลี่ยนไดเรกทอรีการทำงานเป็น other/
แล้วบิลด์ ควรสร้างโดยไม่มี
$ cd ../other
$ bazel build :rv32i_sim
ในไดเรกทอรีดังกล่าว จะมีหน้าต่าง "สวัสดีโลก" โปรแกรมในไฟล์
hello_rv32i.elf
หากต้องการเรียกใช้เครื่องจำลองในไฟล์นี้และดูผลลัพธ์ ให้ทำดังนี้
$ bazel run :rv32i_sim -- other/hello_rv32i.elf
คุณจะเห็นบางสิ่งอยู่บนบรรทัดต่อไปนี้
INFO: Analyzed target //other:rv32i_sim (0 packages loaded, 0 targets configured).
INFO: Found 1 target...
Target //other:rv32i_sim up-to-date:
bazel-bin/other/rv32i_sim
INFO: Elapsed time: 0.203s, Critical Path: 0.00s
INFO: 1 process: 1 internal.
INFO: Build completed successfully, 1 total action
INFO: Running command line: bazel-bin/other/rv32i_sim other/hello_rv32i.elf
Starting simulation
Hello World
Simulation done
$
นอกจากนี้ เครื่องจำลองยังเรียกใช้ในโหมดอินเทอร์แอกทีฟได้โดยใช้คำสั่ง bazel
run :rv32i_sim -- -i other/hello_rv32i.elf
นี่เป็นการแสดง
Command Shell พิมพ์ help
ที่ข้อความแจ้งเพื่อดูคำสั่งที่ใช้ได้
$ bazel run :rv32i_sim -- -i other/hello_rv32i.elf
INFO: Analyzed target //other:rv32i_sim (0 packages loaded, 0 targets configured).
INFO: Found 1 target...
Target //other:rv32i_sim up-to-date:
bazel-bin/other/rv32i_sim
INFO: Elapsed time: 0.180s, Critical Path: 0.00s
INFO: 1 process: 1 internal.
INFO: Build completed successfully, 1 total action
INFO: Running command line: bazel-bin/other/rv32i_sim -i other/hello_rv32i.elf
_start:
80000000 addi ra, 0, 0
[0] > help
quit - exit command shell.
core [N] - direct subsequent commands to core N
(default: 0).
run - run program from current pc until a
breakpoint or exit. Wait until halted.
run free - run program in background from current pc
until breakpoint or exit.
wait - wait for any free run to complete.
step [N] - step [N] instructions (default: 1).
halt - halt a running program.
reg get NAME [FORMAT] - get the value or register NAME.
reg NAME [FORMAT] - get the value of register NAME.
reg set NAME VALUE - set register NAME to VALUE.
reg set NAME SYMBOL - set register NAME to value of SYMBOL.
mem get VALUE [FORMAT] - get memory from location VALUE according to
format. The format is a letter (o, d, u, x,
or X) followed by width (8, 16, 32, 64).
The default format is x32.
mem get SYMBOL [FORMAT] - get memory from location SYMBOL and format
according to FORMAT (see above).
mem SYMBOL [FORMAT] - get memory from location SYMBOL and format
according to FORMAT (see above).
mem set VALUE [FORMAT] VALUE - set memory at location VALUE(1) to VALUE(2)
according to FORMAT. Default format is x32.
mem set SYMBOL [FORMAT] VALUE - set memory at location SYMBOL to VALUE
according to FORMAT. Default format is x32.
break set VALUE - set breakpoint at address VALUE.
break set SYMBOL - set breakpoint at value of SYMBOL.
break VALUE - set breakpoint at address VALUE.
break SYMBOL - set breakpoint at value of SYMBOL.
break clear VALUE - clear breakpoint at address VALUE.
break clear SYMBOL - clear breakpoint at value of SYMBOL.
break clear all - remove all breakpoints.
help - display this message.
_start:
80000000 addi ra, 0, 0
[0] >
บทแนะนำนี้จบลงเพียงเท่านี้ เราหวังว่าข้อมูลนี้จะเป็นประโยชน์