跳转至

PerfCCT usage

原理:

perfCCT框架主要用来收集指令执行过程中的各种信息

比如指令的完整生命周期跟踪,指令的执行结果、访存地址等信息

perfCCT是RTL仿真友好的,它将大规模性能数据存放在c++端的结构体内而不是用reg存放

作为代价,每条指令都需要在取指阶段分配一个sequence number作为唯一标识符,几乎不会影响仿真性能

Gem5端使用方法

以下是gem5端 perfCCT相关接口

createMeta在fetch阶段给每条dyninst分配一个计数器表项,直接复用dyninst本身的seqNum

updateInstPos则会用来记录指令的在何时到达了什么位置,需要在正确的指令流水线各个阶段调用updateInstPos来更新指令的位置时间

updateInstMeta则会用来记录指令除了位置信息以外的性能计数器,比如在写回阶段记录指令的结果,在lsu里记录指令的访存地址等

commitMeta则是在指令退休时将指令的信息dump到archdb中,便于分析

以上接口调用均需要指令的seqNum来查找指令对应的性能计数器表项,在不使用perfCCT时,计数器表项不占任何内存

void createMeta(const DynInstPtr inst);

void updateInstPos(InstSeqNum sn, const PerfRecord pos);

void updateInstMeta(InstSeqNum sn, const PerfRecord meta, uint64_t val);

void commitMeta(InstSeqNum sn);

开启perfCCT

首先需要开启gem5的archdb,再在 configs/common/xiangshan.py 里做如下修改

将dump_lifetime改为True即可

test_sys.arch_db = ArchDBer(arch_db_file=args.arch_db_file)
test_sys.arch_db.dump_from_start = args.arch_db_fromstart
test_sys.arch_db.enable_rolling = args.enable_rolling
test_sys.arch_db.dump_l1_pf_trace = False
test_sys.arch_db.dump_mem_trace = False
test_sys.arch_db.dump_l1_evict_trace = False
test_sys.arch_db.dump_l2_evict_trace = False
test_sys.arch_db.dump_l3_evict_trace = False
test_sys.arch_db.dump_l1_miss_trace = False
test_sys.arch_db.dump_bop_train_trace = False
test_sys.arch_db.dump_sms_train_trace = False
test_sys.arch_db.dump_lifetime = False -> True

此外还需要同时打开--enable-arch-db, 指定db-file

./build/RISCV/gem5.debug ./configs/example/kmhv3.py --raw-cpt --generic-rv-cpt=/nfs/home/yanyue/tools/nexus-am-xs/tests/cputest/build/ifuwidth-riscv64-xs.bin --enable-arch-db --arch-db-file=m5out/test.db

python3 util/perfcct.py m5out/test.db --zoom 1.5 -p 333 --visual | less

分析脚本

gem5的perfCCT分析脚本位于util/perfcct.py中

gem5生成出数据库后,使用以下命令来dump出可读数据

python3 util/perfcct.py test.db

其他选项

--zoom (-z) 尺度缩放

--visual (-v) 文本可视化

--period (-p) tick per cycle, RTL上是1, GEM5上3GHz设置为333, (若是2Ghz设置为500)

举例:

python3 util/perfcct.py test.db --z 1.5 -p 333

若添加“--v”参数,则如下

其中每个点和每一个字母代表一个周期

f代表指令处于fetch或者ibuffer中

d代表指令处于decode

r代表处于rename

D代表处于DispQue

i代表处于issueQue

a代表处于issue0(og0/arb)

g代表处于issue1(og1/read regfile)或者issue2/og2(如果有的话)

e代表处于FU

b代表处于bypassing,写回的前一个周期,对于单周期指令,e和b会重合,此时字母只会输出b

w代表指令已写回,等待提交,可能会出现一连串的‘w’, 代表指令已完成但还未提交

c代表指令已提交

RTL端使用方法

目前 perfcct ~~还处于 PR 状态 (~~~~https://github.com/OpenXiangShan/XiangShan/pull/4007~~~~),处于 PerfCCT 分支,需要在最新的香山中~~

  • ~~先在 difftest 下,merge PerfCCT 分支中的 difftest~~
  • ~~在 utility 下,merge PerfCCT 分支中的 utility~~
  • ~~最后在 xiangshan 下,merge PerfCCT 分支~~

perfcct已经合入主线

编译香山,带上 WITH_CHISELDB=1

执行 emu 时,带上 --dump-db --dump-select-db "lifetime",生成一个 db 文件

在大机房上:

import sqlite3 as sql
import argparse
import subprocess


parser = argparse.ArgumentParser()
parser.add_argument('sqldb')
parser.add_argument('-v', '--visual', action='store_true', default=False)
parser.add_argument('-z', '--zoom', action='store', type=float, default=1)
parser.add_argument('-p', '--period', action='store', default=333)

args = parser.parse_args()

sqldb = args.sqldb

tick_per_cycle = int(args.period)
cycle_per_line = int(100 * args.zoom)

stages = ['f','d','r','D','i','a','g','e','b','w','c']

inst_translate_map = {}

def DisAssemble(val):
    # print(val)
    hex_val = hex(val).lower()
    command = f'echo "DASM({hex_val})" | spike-dasm'
    asm = subprocess.run(command, shell=True, capture_output=True,
                         text=True, check=True).stdout.strip()
    # print(val, hex_val, asm)
    return asm

def non_stage():
    return '.'

def stage(x):
    return stages[x]

def dump_visual(pos, records):
    pos_start = pos[0] % cycle_per_line
    line = ''
    line += '[' + non_stage() * pos_start
    pos_next = pos_start
    last_index = 0
    for i in range(1, len(pos)):
        if (pos[i] == pos[last_index]) or pos[i] == 0:
            continue
        if pos[i] - pos[last_index] >= cycle_per_line - pos_next:
            diff = cycle_per_line - pos_next
            line += f'{stage(last_index)}' * diff + ']\n'
            diff_line = ((pos[i] - pos[last_index]) - diff - 1) // cycle_per_line
            if diff_line > 0:
                line += '[' + f'{stage(last_index)}' * cycle_per_line + ']\n'

            pos_next = pos[i] % cycle_per_line
            line += '[' + f'{stage(last_index)}' * pos_next
        else:
            diff = pos[i] - pos[last_index]
            pos_next = pos[i] % cycle_per_line
            line += f'{stage(last_index)}' * diff
        last_index = i
    if cycle_per_line - pos_next == 0:
        line += ']\n'
        line += f'[{stage(i)}{non_stage() * (cycle_per_line - 1)}]\n'
    else:
        line += f'{stage(i)}' + non_stage() * (cycle_per_line - pos_next - 1) + ']'
    line += str(records)
    print(line)


def dump_txt(pos, records):
    for i in range(len(pos)):
        print(f'{stage(i)}{pos[i]}', end=' ')
    print(records)


dump = dump_txt
if args.visual:
    dump = dump_visual

with sql.connect(sqldb) as con:
    cur = con.cursor()
    cur.execute("SELECT * FROM LifeTimeCommitTrace")
    col_name = [i[0] for i in cur.description]
    col_name = col_name[1:]
    col_name = [i.lower() for i in col_name]
    rows = cur.fetchall()
    for row in rows:
        row = row[1:]
        pos = []
        records = []
        i = 0
        for val in row:
            if col_name[i].startswith('at'):
                pos.append(val//tick_per_cycle)
            elif col_name[i].startswith('pc'):
                if val < 0:
                    val = val + 1 << 64
                records.append(hex(val))
            else:
                if val not in inst_translate_map:
                    inst_translate_map[val] = DisAssemble(val)
                asm = inst_translate_map[val]
                records.append(asm)
            i += 1
        dump(pos, records)
import re
from collections import Counter
import sys
import gzip

def parse_instruction(line):
    # something like [c1000 'c_addw a4 a5', '0x800000']
    match = re.search(r"c(\d+) \['([^']+)',\s*'([^']+)'\]", line)
    if match:
        return match.groups()   # commit time + instruction + pc
    return None

def is_branch_instruction(instr):
    branch_instructions = ['beq', 'bne', 'blt', 'bge', 'bltu', 'bgeu', 'beqz', 'bnez', 'j', 'jal', 'jalr', 'ret', 'mret', 'sret']
    branch_instructions += ['c_beqz', 'c_bnez', 'c_j', 'c_jal', 'c_jr', 'c_jalr']
    return any(instr.split()[0].startswith(branch) for branch in branch_instructions)

def analyze_commit_trace(file_path):
    basic_blocks = Counter()
    basic_blocks_commit_time = Counter()
    current_block = []
    block_begin_commit_time = 0

    open_func = gzip.open if file_path.endswith('.gz') else open
    mode = 'rt' if file_path.endswith('.gz') else 'r'

    with open_func(file_path, mode) as file:
        commit_time = 0
        for line in file:
            parsed = parse_instruction(line) # commit time[0] + instruction[1] + pc[2]
            if parsed:
                if len(current_block) == 0:
                    block_begin_commit_time = int(parsed[0])
                commit_time = int(parsed[0])
                current_block.append((parsed[1], parsed[2]))
                if is_branch_instruction(parsed[1]):
                    if current_block:
                        # print(current_block)
                        basic_blocks[tuple(current_block)] += 1
                        basic_blocks_commit_time[tuple(current_block)] += int(parsed[0]) - block_begin_commit_time
                        current_block = []

        if current_block:
            basic_blocks[tuple(current_block)] += 1
            basic_blocks_commit_time[tuple(current_block)] += commit_time - block_begin_commit_time
    return basic_blocks, basic_blocks_commit_time

def main(file_path):
    basic_blocks, basic_blocks_commit_time = analyze_commit_trace(file_path)

    print("Top 10 most common basic blocks:")
    total_count = sum(basic_blocks.values())
    total_cycle = sum(basic_blocks_commit_time.values())
    print(f"Total cycle: {total_cycle}")
    for block, count in basic_blocks.most_common(10):
        percentage = (count / total_count) * 100
        print(f"Count: {count} ({percentage:.2f}%) cycle: {basic_blocks_commit_time[block]}", end='')
        print(f" ratio ({basic_blocks_commit_time[block] / total_cycle * 100:.2f}%)")
        print("Instructions:")
        for instr, pc in block:
            print(f"  {pc}: {instr}")
        print()

if __name__ == "__main__":
    if len(sys.argv) < 2:
        print("Please provide the file path as a command line argument.")
        sys.exit(1)
    file_path = sys.argv[1]
    main(file_path)
from collections import Counter
import sqlite3 as sql
import argparse
import numpy as np
import subprocess
from tqdm import tqdm


StageNameShort = ['f', 'd', 'r', 'D', 'i', 'a', 'g', 'e', 'b', 'w', 'c']
StageNameLong = ['fetch', 'decode', 'rename', 'dispatch', 'issue',
                 'arb', 'read', 'execute', 'bypass', 'writeback', 'commit']


def DisAssemble(val):
    # print(val)
    hex_val = hex(val).lower()
    command = f'echo "DASM({hex_val})" | spike-dasm'
    asm = subprocess.run(command, shell=True, capture_output=True,
                         text=True, check=True).stdout.strip()
    # print(val, hex_val, asm)
    return asm


def ReadDB(sqldb, start_clock: int, end_clock: int, period: int, inter_gap: bool, inner_gap: bool):
    inst_pos_clock = []
    inst_records = []
    inst_translate_map = {}

    with sql.connect(sqldb) as con:
        cur = con.cursor()

        clock_pick_cmd = f"WHERE AtCommit >= {start_clock*period} "
        if end_clock >= start_clock:
            clock_pick_cmd += f"AND AtCommit <= {end_clock*period} "

        cur.execute(
            f"SELECT * FROM LifeTimeCommitTrace {clock_pick_cmd} ORDER BY ID ASC")
        col_name = [i[0].lower() for i in cur.description[1:]]
        rows = cur.fetchall()

    for row in tqdm(rows, desc='Reading DB'):
        row = row[1:]
        pos_clock_cycles = []
        pos_index = 0
        pc = None
        asm = None

        for val in row:
            # print(f"{col_name[pos_index]}, {val}")
            if col_name[pos_index].startswith('at'):
                pos_clock_cycles.append(float(val//period))
            elif col_name[pos_index].startswith('pc'):
                if val < 0:
                    # pc is unsigned, but sqlite3 only supports signed integer [-2^63, 2^63-1]
                    # if real pc > 2^63-1, it will be stored as negative number (real pc - 2^64)
                    # when read a negtive pc, real pc = negtive pc + 2^64
                    val = val + (1 << 64)
                pc = hex(val)
            elif col_name[pos_index].startswith('disasm'):
                if args.platform == 'rtl':
                    if val not in inst_translate_map:
                        inst_translate_map[val] = DisAssemble(val)
                    asm = inst_translate_map[val]
                else:
                    asm = val
            pos_index += 1

        inst_pos_clock.append(pos_clock_cycles)
        inst_records.append(tuple([pc, asm]))

    if inter_gap:
        inst_pos_clock = np.diff(inst_pos_clock, axis=0)
    elif inner_gap:
        inst_pos_clock = np.diff(inst_pos_clock, axis=1)

    inst_records = inst_records[len(inst_records) - len(inst_pos_clock):]

    inst_clock_info: dict = {}
    for inst_index in range(len(inst_records)):
        if inst_records[inst_index] not in inst_clock_info:
            inst_clock_info[inst_records[inst_index]] = []
        inst_clock_info[inst_records[inst_index]].append(
            inst_pos_clock[inst_index])

    inst_avg_clock_info: dict = {}
    for key in inst_clock_info.keys():
        inst_avg_clock_info[key] = np.mean(inst_clock_info[key], axis=0)

    return inst_records, inst_pos_clock, inst_avg_clock_info


def IsBranchInst(instr: str) -> bool:
    branch_instructions = ['beq', 'bne', 'blt', 'bge', 'bltu',
                           'bgeu', 'beqz', 'bnez', 'j', 'jal', 'jalr', 'ret']
    branch_instructions += ['c_beqz', 'c_bnez',
                            'c_j', 'c_jal', 'c_jr', 'c_jalr']
    return any(instr.split()[0].startswith(branch) for branch in branch_instructions)


def ExtractBasicBlocks(pc_inst_list: tuple[str, str]) -> Counter:
    basic_blocks = Counter()
    current_block = []

    for i, (pc, inst) in tqdm(enumerate(pc_inst_list), desc='Analyzing Traces'):
        current_block.append((pc, inst))
        if IsBranchInst(inst) and current_block:
            basic_blocks[tuple(current_block)] += 1
            current_block = []
    if current_block:
        basic_blocks[tuple(current_block)] += 1

    return basic_blocks


def bbl_main(inst_info, inst_avg_clock_info, inter_gap, inner_gap):
    basic_blocks = ExtractBasicBlocks(inst_info)

    mode_str = "inter-gap" if inter_gap else "inner-gap" if inner_gap else "normal"

    print(f"Top 10 most common basic blocks (mode:{mode_str}):")
    for block, count in basic_blocks.most_common(10):
        print()
        print(f"Count: {count}")
        print("Instructions:")
        pc_header = f"{'PC':18}"
        instr_header = f"{'Instruction':30}"
        clock_head = ":".join([f"{stage_name:>9}" for stage_name in StageNameLong])

        if inter_gap or inner_gap:
            header = f"  {pc_header} : {instr_header} : {clock_head}"
        else:
            header = f"  {pc_header} : {instr_header} "

        print(header)

        for pc, instr in block:
            if inter_gap or inner_gap:
                formatted_clock = [
                    f"{clock:9.2f}" for clock in inst_avg_clock_info[(pc, instr)]]
                formatted_clock = ":".join(formatted_clock)
                print(f"  {pc:18} : {instr:30} : {formatted_clock}")
            else:
                print(f"  {pc:18} : {instr:30} ")


def perfcct_main(inst_info, inst_pos_clock_info, start_pc, end_pc, attention_pc: list[str], only_attention: bool):

    for i, (pc, asm) in enumerate(inst_info):
        pos = inst_pos_clock_info[i]

        if not only_attention and \
                (int(pc, 16) < int(start_pc, 16) or int(pc, 16) > int(end_pc, 16)):
            continue

        if only_attention and (pc not in attention_pc):
            continue

        print(f"{pc:18} : {asm:30}", end=' : ')

        for j, pos_clock in enumerate(pos):
            print(f'{StageNameShort[j]} {int(pos_clock)}', end=' : ')

        if pc in attention_pc:
            print("<<====", end=' ')

        print()
        if pc == end_pc:
            print()


if __name__ == "__main__":
    parser = argparse.ArgumentParser()
    parser.add_argument('sqldb', action='store',
                        help='Path to the SQLite database')
    parser.add_argument('-p', '--period', action='store',
                        type=int,
                        default=333,
                        help='Number of ticks per clock cycle')
    parser.add_argument('-P', '--platform', action='store',
                        choices=['gem5', 'rtl'],
                        default='gem5',
                        help='Platform to analyze')
    parser.add_argument('-s', '--start-clock', action='store',
                        type=int,
                        default=0,
                        help='Start clock to analyze')
    parser.add_argument('-e', '--end-clock', action='store',
                        type=int,
                        default=-1,
                        help='End clock to analyze')
    parser.add_argument('-n', '--num-insts', action='store',
                        type=int,
                        default=-1,
                        help='MAX Number of instructions to analyze')
    parser.add_argument('--inter-gap', action='store_true',
                        default=False,
                        help='Analyze inter-gap')
    parser.add_argument('--inner-gap', action='store_true',
                        default=False,
                        help='Analyze inner-gap')
    parser.add_argument('--tool', action='store',
                        choices=['perfcct', 'bbl'],
                        default='bbl',
                        help='Mode to analyze')
    parser.add_argument('--attention-pc', action='store',
                        nargs='+',
                        default=[],
                        help='Attention PC')
    parser.add_argument('--start-pc', action='store',
                        type=str,
                        default="0x0",
                        help='Start PC to analyze, a hex value start with 0x')
    parser.add_argument('--end-pc', action='store',
                        type=str,
                        default="0xffffffffffffffff", help='End PC to analyze, a hex value start with 0x')
    parser.add_argument('--only-attention', action='store_true',
                        default=False,
                        help='Only print attention PC')

    args = parser.parse_args()

    if args.platform.lower() == 'rtl':
        args.period = 1

    if args.inter_gap and args.inner_gap:
        raise ValueError("Cannot set both inter-gap and inner-gap to True")

    inst_info, inst_pos_clock_info, inst_avg_clock_info = ReadDB(
        args.sqldb, args.start_clock, args.end_clock, args.period, args.inter_gap, args.inner_gap)

    if args.tool == 'perfcct':
        perfcct_main(inst_info, inst_pos_clock_info, args.start_pc,
                     args.end_pc, args.attention_pc, args.only_attention)
    elif args.tool == 'bbl':
        bbl_main(inst_info, inst_avg_clock_info,
                 args.inter_gap, args.inner_gap)
# 使用文本格式输出
python3 perfcct.py <db path> --zoom 2 -p 1 | gzip > trace.gz
# 得到 hotloop 信息
python3 hotloop.py trace.gz > hotloop.txt
# 分析指令之间的 inter-gap
python3 ClockAnalysis.py <db path> -p 1 -P rtl --tool bbl --inter-gap > inter_gap.txt
# 分析指令内的 inner-gap
python3 ClockAnalysis.py <db path> -p 1 -P rtl --tool bbl --inner-gap > inner_gap.txt
# 可视化指令执行的时空图
python3 perfcct.py <db path> --zoom 2 -p 1 -v | less

遇到问题请及时联系@Lurker@邱泽源@常庆宇@李昕

RTL端添加自定义计数器

在utility/src/ChiselPerfCCT.scala中,能找到如下接口

其中tick,createInstMetaAtFetch,updateInstPos,commitInstMeta这几个接口是固定接口外

只有updateInstMeta是用户可自定义的接口,

其传参规则如下:

sn:指令的seqNum

meta:一个枚举类id,表示指令的meta标记

data:表示需要写入的meta的值

样例

若我们想记录一条指令的写回的值,首先我们需要在InstRecord里添加一个meta,这里记为Result

修改对应的c++代码,添加数据库

修改对应的c++函数

然后在RTL chisel代码的正确位置调用updateInstMeta(只是举例,不代表真实RTL代码)

最后开启perfcct即可在数据库中dump 指令的写回值

!!另外注意,为了削减数据库的大小,对于“访存地址”这类不是所有指令都有的meta,建议单独开一个数据库!!