บทแนะนำฟังก์ชันอรรถศาสตร์ในการสอน

วัตถุประสงค์ของบทแนะนำนี้ได้แก่

  • เรียนรู้การใช้ฟังก์ชันความหมายเพื่อนำความหมายของการสอนไปใช้
  • เรียนรู้ว่าฟังก์ชันความหมายเกี่ยวข้องกับคำอธิบายตัวถอดรหัส 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] >

บทแนะนำนี้จบลงเพียงเท่านี้ เราหวังว่าข้อมูลนี้จะเป็นประโยชน์