8. genattr 工具
8.1. 概览
Genattr 将从机器描述文件输出 insn-attr.h 。在机器描述文件中,除了 define_insn , define_expand , define_split , define_peephole 及 define_peephole2 这些通过 rtx 指令合并来显示优化机会的模式外,关于硬件架构的模式也会出现。它们是 define_delay , define_function_unit , define_insn_reservation , define_cpu_unit 等。对于这些架构信息,它们被保存在目录“ gcc-3.46/gcc/config/i386” 下特定的机器描述文件里。在那里,我们可以找到两个文件: athlon.md 以及 pentium.md 。
在这个工具里,将遇到三个关于硬件的模式: define_delay , define_function_unit 及 define_insn_reservation 。不过,对于 athon 及 pentium ,都没有 define_delay 。
不管如何,让我们依次来看一下它们。
8.1.1. DEFINE_DELAY 模式的概览
以下摘自 gccinfo 。
一个指令被称为要求一个“延迟槽( delay slot )”,如果某些物理上位于这个指令之后的指令,就仿佛位于这个指令之前那样执行。典型的例子有跳转( branch )及调用( call )指令,通常在这个跳转或调用指令执行前,执行紧跟随后的指令。
在某些机器上,条件跳转指令可以可选择地“取消”在延迟槽内的指令。这意味着对于特定的跳转结果,这个指令将不会执行。如果跳转为真取消指令,及如果跳转为假取消指令,两者都被支持。
延迟槽调度与指令调度的差异在于,确定一个指令是否需要一个延迟槽,仅依赖于将被产生的指令的类型,而不是指令间的数据流。
一个指令需要一个或多个延迟槽的要求,由表达式 define_delay 来显示。它具有如下的形式:
(define_delay TEST
[DELAY-1 ANNUL-TRUE-1 ANNUL-FALSE-1
DELAY-2 ANNUL-TRUE-2 ANNUL-FALSE-2
...])
TEST 是一个属性测试,它显示这个 define_delay 是否应用于一个特别的指令。如果是这样,所要求的延迟槽的数目,由作为第二个参数的向量的长度来决定。一个放置在延迟槽 N 的指令必须满足属性测试 DELAY-N 。 ANNUL-TRUE-N 是一个属性测试,它指出如果这个跳转为真,哪个指令可能被取消。类似的, ANNUL-FALSE-N 指出如果这个跳转为假,哪个指令可能被取消。如果对于这个延迟槽不支持取消,应该写入 `(nil)' 。
例如,在一个通常的情形下,跳转及调用指令要求一个延迟槽,它可以包含除了跳转及调用以外的任意指令,以下将被写入相应的 md' 文件中:
(define_delay (eq_attr "type" "branch,call")
[(eq_attr "type" "!branch,call") (nil) (nil)])
有可能指定多个 define_delay 表达式。在这个情形下,每个这样的表达式指出不同的延迟槽的要求,并且必须没有指令能令两个 define_delay 表达式的测试同时为 true 。
例如,如果有一个机器,对于跳转要求一个延迟槽,对于调用则是两个,延迟槽不能包含跳转或调用指令,并且在延迟槽中,对于跳转任意有效的指令可以被取消,如果跳转为真,我们可以如下来表示:
(define_delay (eq_attr "type" "branch")
[(eq_attr "type" "!branch,call")
(eq_attr "type" "!branch,call")
(nil)])
(define_delay (eq_attr "type" "call")
[(eq_attr "type" "!branch,call") (nil) (nil)
(eq_attr "type" "!branch,call") (nil) (nil)])
8.1.2. DEFINE_FUNCTION_UNIT 模式的概览
以下摘自 gccinfo 。
在绝大多数 RISC 机器上,存在指令,其结果对于一个指定数目的周期不可用。通常的情形是从内存载入数据的指令。在许多机器上,如果该数据在这个载入指令后太快引用,将导致一个流水线停转( pipeline stall )。
另外,许多更新的处理器具有多个功能单元,通常一个用于整型而另一个用于浮点,并且经常会导致流水线停转,当一个需要的结果还没有准备好的时候。
一个机器被分解成“功能单元”,每个单元以先进先出的次序,执行一个特定类别的指令。每个周期接受一条指令,并且允许一个结果被用于紧接着的指令(通常通过转递( forwarding ))的功能单元不需要被指出。典型的 RISC 处理器一般来说具有单个功能单元,我们可以称之为 memory 。更新的超标量( superscalar )处理器通常具有用于浮点操作的功能单元,通常至少一个浮点加法器及乘法器。
一个类别指令每次对一个功能单元的使用,通过一个 define_function_unit 表达式来指出,它看起来像这样:
(define_function_unit NAME MULTIPLICITY SIMULTANEITY
TEST READY-DELAY ISSUE-DELAY
[CONFLICT-LIST])
NAME 是一个给出了该功能单元名字的字符串。
MULTIPLICITY 是一个整数,它指出了在这个处理器中相同单元的数目。如果指定了对于一个的单元,它们将被独立地调度。仅真正独立的单元才应该被计入;一个流水线单元应该被说明为单个单元(对于单个指令类别,具有多个真正独立,并且非流水线的功能单元的机器,仅有的常见的例子是 CDC 6600 的两个乘法及两个增量单元)。
SIMULTANEITY 指明,在每个该功能单元实例中,同时可以执行的指令的最大数目。它是 0 ,如果该单元是流水线化的,并且没有限制。
所有引用功能单元 NAME 的 define_function_unit 的定义,必须具有相同的名字,及相同的 MULTIPLICITY 以及 SIMULTANEITY 的值。
TEST 是一个,选择我们在这个定义中描述的指令的,属性测试 注意到一个指令可能用在多个功能单元中,并且一个功能单元可能被多个 define_function_unit 所指定。
READY-DELAY 是一个整数,它指出在该周期数之后,该指令的结果可以被使用,而不会导致任何停转。
ISSUE-DELAY 是一个整数,它指出一个周期数,在匹配这个 TEST 表达式的指令,开始使用这个功能单元,直达这个周期数之后,后序指令才可以开始。代价 N 表示一个 N-1 周期的延迟。一个后续的指令可能也会被延迟,如果之前的一个指令具有一个大于 READY-DELAY 的值。这个阻塞的效果,使用项 SIMULTANEITY , READY-DELAY , ISSUE-DELAY 及 CONFLICT-LIST ,来计算。对于一个普通的非流水线化的功能单元, SIMULTANEITY 是 1 ,该单元被执行的指令所占据,阻塞 READY-DELAY 个周期,而更小的 ISSUE-DELAY 的值被忽略。
CONFLICT-LIST 是一个可选的,给出这个单元的详细冲突代价的列表。如果指定了,它是一个应用于在 NAME 中被选择执行指令的条件测试列表,它跟在已经在 NAME 中执行,匹配 TEST 的特定指令之后。对于每个在这个列表中的指令, ISSUE-DELAY 指出冲突的代价;对于不在列表中的指令,这个代价是 0 。如果没有指明, CONFLICT-LIST 默认适用于使用该功能单元的所有指令。这个向量的典型使用出现在,一个浮点功能单元可以流水线化单精度或双精度操作,但不是两者同时,或一个内存单元可以流水线化载入,但不流水线化保存,等等。
作为一个例子,考虑一个典型的 RISC 机器,其中一条载入指令的结果在两个周期内不可用(要求一个单“延迟”指令),并且同时只能执行一条载入指令。这可以被显示为:
(define_function_unit "memory" 1 1 (eq_attr "type" "load") 2 0)
对于一个可以流水线化单精度或双精度,但不能共存的浮点功能单元的情形,可以按以下显示:
(define_function_unit
"fp" 1 0 (eq_attr "type" "sp_fp") 4 4 [(eq_attr "type" "dp_fp")])
(define_function_unit
"fp" 1 0 (eq_attr "type" "dp_fp") 4 4 [(eq_attr "type" "sp_fp")])
注意,调度器尝试避免功能单元的冲突,并且使用在这个 define_function_unit 表达式中的所有规范。最近发现,这些规范可能不允许模仿某些更新的,具有使用多个流水线化单元指令的超标量处理器。对于第二个被使用的单元,在它们执行期间,这些指令将导致一个潜在的冲突,并且没有方法来表示这个冲突。
8.1.3. DEFINE_INSN_RESERVATION 模式的概览
这部分的模式与流水线各系密切。以下摘自 gccinfo 。
这部分描述了基于处理器流水线描述的自动机的构建。在该机器描述文件中,构建的次序是无关重要的。
以下可选的构建描述了所产生的自动机的名字,并用于流水线危险的识别。有时所产生的,由流水线危险识别器使用的有限状态机很大。如果我们使用多于一个自动机,并把功能单元绑定到这些自动机,这些自动机的总体大小通常小于单个自动机的大小。如果不存在这样的一个构造,仅有一个有限状态自动机被产生。
(define_automaton AUTOMATA-NAMES)
AUTOMATA-NAMES 是给出自动机名字的字符串。名字由逗号分隔。所有的自动机应该具有唯一的名字。自动机的名字被用在 define_cpu_unit 及 define_query_cpu_unit 的构造中。
用在指令预约描述中的每个处理器功能单元,应该由以下的构造所描述。
(define_cpu_unit UNIT-NAMES [AUTOMATON-NAME])
UNIT-NAMES 是一个给出由逗号分隔的功能单元的名字的字符串。不要使用名字“ nothing ”,它被保留作其它用途。
AUTOMATON-NAME 是一个给出这个单元所绑定的自动机的名字的字符串。这个自动机应该在 define_automaton 构造中描述。你应该给出“ automaton-name ”,如果存在一个已定义的自动机。
把功能单元赋给自动机,受这些单元在指令预约中使用的限制。最重要的限制是:如果一个预订单元出现在一个指令预约替代者( alternative )的一个特定周期中,那么来自这个自动机的某些单元必须出现在这个指令预约的其它替代者的同一周期中。其它的限制,在后续构造的描述中会提及。
以下的构造,类似于 define_cpu_unit ,描述了 CPU 的功能单元。这样的单元预定( reservation )可以查询一个自动机的状态。指令调度器不会对给定的自动机状态来查询功能单元的预定。因此作为一个规则,你不需要这个构造。这个构造将被用于将来代码生成(即,产生 VLIW 指令模板)。
(define_query_cpu_unit UNIT-NAMES [AUTOMATON-NAME])
UNIT-NAMES 是一个给出由逗号分隔的功能单元名字的字符串。
AUTOMATON-NAME 是一个给出这个单元所绑定的自动机名字的字符串。
以下的构造是描述一条指令流水线属性的主要构造。
(define_insn_reservation INSN-NAME DEFAULT_LATENCY
CONDITION REGEXP)
DEFAULT_LATENCY 是一个给出该指令延迟时间的值。在旧的描述及基于自动机的流水线描述之间,有一个重要的差异。当我们使用旧的描述时,这个延迟时间被用于所有的依赖部分( dependencies )。在基于自动机的流水线描述中,给定的延迟时间仅用于真正的依赖部分。非依赖部分的代价总是 0 ,并且输出依赖的代价是产生指令及消耗指令间延迟时间的差异( 如果这个差异是附属,这个代价被视为 0 )。你总是可以通过使用目标平台钩子 TARGET_SCHED_ADJUST_COST ,修改任意描述的默认代价。
INSN-NAME 是一个给出该指令内部名字的字符串。这个内部名字被用在构造 define_bypass 中,以及为调试目的产生的自动机描述文件中。这个内部名字与 define_insn 中的名字没有共同之处。使用在处理器手册中描述的指令类别( class )是一个好的做法。
CONDITION 定义了哪些 RTL 指令由这个构造来描述。应该记住,如果对于一条指令,两个或更多不同的 define_insn_reservation 构造的 CONDITION 是 true ,将会有大麻烦。在这个情况下,这个指令使用那个预订,是没有定义的。这样的情形,在流水线危险识别器的生成过程中,是不作检查的,因为一般而言,识别出两个可能具有相同值的条件,是相当困难的(特别地,如果条件包含 symbol_ref )。而且流水线危险识别器亦不对此作检查,因为它将显著降低识别器的速度。
REGEXP 是一个描述由这个指令所预订的 cpu 功能单元的字符串。预订由符合以下语法的一个正则表达式所描述:
regexp = regexp "," oneof
| oneof
oneof = oneof "|" allof
| allof
allof = allof "+" repeat
| repeat
repeat = element "*" number
| element
element = cpu_function_unit_name
| reservation_name
| result_name
| "nothing"
| "(" regexp ")"
‘ , ’用于描述在预订中下一个周期的开始。
‘ | ’用于描述一个由,第一个正则表达式描述的预订,或者由第二个正则表达式描述的预订,或等等,组成的预订。
‘ + ’用于描述一个由,第一个正则表达式描述的预订,及由第二个正则表达式描述的预订,及等等,组成的预订。
‘ * ’用于方便的目的,仅表示一个序列,其中,伴随周期的推进,该正则表达式重复 NUMBER 次(参考‘ , ’)。
‘ cpu_function_unit_name ’表示具名功能单元的预订。
‘ reservation_name ’——参见 define_reservation 构造的描述。
‘ nothing ’表示没有单元预订。
有时候不同指令的单元预订包含相同的部分。在这种情况下,可以通过以下的构造描述公共部分,来简化该流水线描述。
(define_reservation RESERVATION-NAME REGEXP)
RESERVATION-NAME 是一个给出 REGEXP 名字的字符串。功能单元名字及预订名字在同一个名字空间里。因此预订名应该不同于功能单元名,并且不能使用名字‘ nothing ’。
以下的构造用于描述给定指令对延迟时间的例外。这也被称为旁路( bypass )。
(define_bypass NUMBER OUT_INSN_NAMES IN_INSN_NAMES
[GUARD])
NUMBER 定义了,何时由在字符串 OUT_INSN_NAMES 中给定的指令所产生的结果,对于由字符串 IN_INSN_NAMES 中给定的指令就绪。在字符串中的指令由逗号分隔。
GUARD 是一个可选的字符串,它给出了一个 C 函数名,这个函数定义了这个旁路的一个额外的保护。这个函数将得到两条指令作为参数。如果这个函数返回 0 ,这个旁路将被忽略。这个额外的保护对于识别复杂的旁路是必要的,即,当一个消费者只是 store 指令的地址(不是被保存的值)。
以下五个构造通常用于描述 VLIW 处理器,或更准确些,描述小( small )指令在 VLIW 指令槽中的布置。它们也可以被用在 RISC 处理器。
(exclusion_set UNIT-NAMES UNIT-NAMES)
(presence_set UNIT-NAMES PATTERNS)
(final_presence_set UNIT-NAMES PATTERNS)
(absence_set UNIT-NAMES PATTERNS)
(final_absence_set UNIT-NAMES PATTERNS)
UNIT-NAMES 是一个给出由逗号分隔的功能单元名的字符串。
PATTERNS 是一个给出由逗号分隔的功能单元模式的字符串。目前,模式( pattern )是一个功能单元或由空格分隔的多个功能单元。
第一个构造( exclusion_set )表示,在第一个字符串中的每个功能单元,不能同时与一个名字出现在第二个字符串中的单元一起预订,反之亦然。例如,该构造对于描述,具有完全流水线化的,能并行执行单精度或双精度指令(但不能混合)的功能单元,是有用的。
第二个构造( presence_set )表示,在第一个字符串中的每个功能单元不能被预订,除非在第二个字符串中的单元,至少有一个被预订了。这是一个非对称的关系。例如,它对于描述 VLIW 架构的槽 1 在槽 0 后面预订是有用的。
我们可以通过以下的构造来描述它
(presence_set "slot1" "slot0")
或者槽 1 仅在槽 0 以及单元 b0 后面预订。在这种情况下,我们可以写作:
(presence_set "slot1" "slot0 b0")
第三个构造( final_presence_set )类似于 presence_set 。它们之间的区别在于何时完成检查。当一条指令在,给定的反映所有当前及计划的单元预订的自动机状态中,发布( issued )时,该自动机的状态发生变化。第一个状态是一个源状态,第二个是一个结果状态。对于 presence_set 的检查,在源状态预订中完成;对于 final_presence_set 的检查,在结果状态预订中完成。这个构造对于描述一个由两个连续预订构成的预订是有用的。例如,如果我们使用
(presence_set "slot1" "slot0")
紧接着的指令将不会被发布(因为槽 1 要求槽 0 ,而槽 0 不出现在源状态中)。
(define_reservation "insn_and_nop" "slot0 + slot1")
但是它可以被发布,如果我们使用相似的 final_presence_set 。
第四个构造( absence_set )表示,在第一个字符串中的功能单元,仅当在第二个字符串中的单元都没有被预订时,才可以预订。这是一个非对称关系(事实上 exclusion_set 类似于这个构造,不过它是对称的)。例如,它对于描述, VLIW 槽 0 不能在槽 1 或槽 2 预订后预订,是有用的。 我们可以通过以下的构造来描述
(absence_set "slot2" "slot0, slot1")
或者,如果槽 0 及单元 b0 被预订,或槽 1 及单元 b1 被预订,槽 2 不能被预订。在这种情形下,我们可以写作
(absence_set "slot2" "slot0 b0, slot1 b1")
所有在一个集合中提到的功能单元应该属于同一个自动机。
最后一个构造( final_absence_set )类似于 absence_set ,不过检查在结果预订(状态)里完成。参见 final_presence_set 的注释。
可以通过以下的构造控制流水线危险识别器的生成器。
(automata_option OPTIONS)
OPTIONS 是一个给出会影响所产生代码选项的字符串。当前有以下选项:
* “ no-minimization ”不最小化自动机。这仅当我们正在调试该描述,并且需要更准确地 查看预订的状态时,才值得这样做。
* “ time ”表示打印出额外的关于自动机生成的时间统计。
* “ v ”表示产生一个描述结果自动机的文件。这个文件具有后缀‘ .dfa ’,并且可被用于验证及调试的描述。
* “ w ”表示对于非关键错误,产生一个警告,而不是错误。
* “ ndfa ”构建非确定性有限状态自动机( nondeterministic finite state automata )。这影响,在正则表达式中,对操作符‘ | ’的处理。这个操作符通常的处理是,尝试第一个候选( alternative ),如果预订是不可能的,才尝试第二个。非确定性的处理意味着尝试所有的候选,它们中的某些可能会被后续指令的预订所拒绝。不能在非确定性自动机状态中查询功能单元预订。
* “ progress ”表示输出进度条,以显示对于正在处理的自动机,已经产生了多少个状态。在调试一个 DFA 描述是,这是有用的。如果你看到太多的状态,应该干预流水线危险识别器的生成器,并尝试找出产生巨大自动机的原因。
作为一个例子,考虑一个超标量 RISC 机器,它可以在一个周期内发布 3 条指令(两条整数指令及一条浮点指令),但同一期间只能完成两条指令。为了描述这点,我们定义如下的功能单元。
(define_cpu_unit "i0_pipeline, i1_pipeline, f_pipeline")
(define_cpu_unit "port0, port1")
所有简单整数指令可以在任意整数流水线上执行 , 并且它们的结果在 两个周期内就绪。简单整数指令被发布入第一个流水线,除非它被预订了; 否则它们被发布入第二个流水线。整数除法及乘法指令只能在第二个整数流水线中执行,并且它们的结果分别在 8 及 4 个周期里就绪。整数除法不能被流水线化,即,后序的整数除法指令,直到当前乘法指令完成之前,不能被发布。浮点指令是充分流水线化的,并且它们的结果在 3 个周期内就绪。若一个整数指令要使用一个浮点指令的结果,将导致额外的一个周期的延迟。为了描述这些,我们可以详细说明为
(define_cpu_unit "div")
(define_insn_reservation "simple" 2 (eq_attr "type" "int")
"(i0_pipeline | i1_pipeline), (port0 | port1)")
(define_insn_reservation "mult" 4 (eq_attr "type" "mult")
"i1_pipeline, nothing*2, (port0 | port1)")
(define_insn_reservation "div" 8 (eq_attr "type" "div")
"i1_pipeline, div*7, div + (port0 | port1)")
(define_insn_reservation "float" 3 (eq_attr "type" "float")
"f_pipeline, nothing, (port0 | port1))
(define_bypass 4 "float" "simple,mult,div")
为了简化描述,我们可以描述如下的预订
(define_reservation "finish" "port0|port1")
并且在所有如下的 define_insn_reservation 构造中使用之
(define_insn_reservation "simple" 2 (eq_attr "type" "int")
"(i0_pipeline | i1_pipeline), finish")