list scheduling algorithm 指令调度 —— 笔记
作者:Yaong
版权:本文版权归作者和博客园共有
转载:欢迎转载,但未经作者同意,必须保留此段声明;必须在文章中给出原文连接;否则必究法律责任
记录一下对 list-scheduling algorithm的学习过程。
指令调度的一般性目的是为了获得更快的程序执行速度。
指令调度器需要满足在执行结果相同的前提下,最小化程序块的执行时间。
指令调度受到三方面的约束,分别是数据的流向,单条指令的执行时间(delay),以及目标处理器的处理能力(多个并行的运算单元)。
数据的流向由DAG图表示。
指令调度需要满足3个基本要求,我们用n表示DAG中的一个node,S(n)表示节点n被调度到的时钟周期,delay(n)表示完成执行节点n所需要的时钟周期数:
1.调度的执行需要在真正执行的开始以后,即S(n) > 0;
2.如果两个节点(n1,n2)间有一条edge相连接,那么需要满足S(n1) + delay(n1) ≤ S(n2),即如果n2依赖n1的执行结果,那么n2不能早于S(n1) + delay(n1)前被调度。
3.指令调度器不能调度超过实际处理器单个时钟周期内能处理的操作类型数。
list-scheduling algorithm通过启发式的方法完成指令的调度。
首先根据需要被调度指令间的依赖关系构建DAG,然后将没有依赖项的指令加入到active列表中,active列表中的项即为准备好的,可选的被调度指令。
然后,依据一定的等级规则(比如指令的delay周期)从active列表中选择指令做调度。当一条指令被调度后,需要更新active列表,将不再有依赖项的指令从DAG中加入active列表中。
持续上述过程,直到指令被调度完成。
如果在调度中出现了active列表为空的情况,可能需要插入nop操作。
为便于描述,假设以下描述的目标处理器,一次只执行一条三元操作指令。
算法步骤:
-
1.构建DAG
-
2.计算 Latency
-
3.指令调度
1、构造DAG
DAG构造需要分析数据流依赖关系。
三种依赖关系:
-
flow dependence or true dependence
-
antidependence
-
output dependence
DAG中的edge表示数据的流向,DAG中的node代表一种operation。
每个node都有两个属性,分别是操作类型和执行该操作所需要的指令周期。
如果两个nodes,n1和n2间通过一条edge相连接,说明在n2中会用到n1的执行结果。
我们通过如下3个结构体来表示dag,dag_node,dag_edge:
struct dag_edge { struct dag_node *child; /* User-defined data associated with the edge. */ void *data; }; struct dag_node { /* Position in the DAG heads list (or a self-link) */ struct list_head link; /* Array struct edge to the children. */ struct util_dynarray edges; uint32_t parent_count; }; struct dag { struct list_head heads; };
首先我们需要初始一个dag,通过dag_create()来完成,struct dag中是一个链表头,通过这个双向链表,将所有待调度的指令连接起来。
struct dag * dag_create(void *mem_ctx) { struct dag *dag = rzalloc(mem_ctx, struct dag); list_inithead(&dag->heads); return dag; }
根据当前需要调度的指令数来,初始化node,如前文所述,单条指令即为一个node。
使用函数dag_init_node()完成对一个node的初始化,并将该node节点加入到dag链表中。
struct dag_node中的成员struct util_dynarray edges表述该node到其他node的edges集合,其是通过动态数组实现的。
/** * Initializes DAG node (probably embedded in some other datastructure in the * user). */ void dag_init_node(struct dag *dag, struct dag_node *node) { util_dynarray_init(&node->edges, dag); list_addtail(&node->link, &dag->heads); }
假设我们有N调指令需要调度,在调用执行 dag_create()、dag_init_node()会形成如下所示的结构,即所有node加入到dag的链表中。
EXAMPLE: struct dag_node node[N]; struct dag *dag = dag_create(NULL); for ( int i = 0; i < N; i++) { dag_init_node(dag, &node[i]); } Map: dag->heads <--> node[0] <--> node[1] <--> node[3] <--> ... <--> node[N]
完成了dag和node的初始化后,接下来需要依据nodes间的依赖关系构建edge,最终形成指令间的DAG。
函数dag_add_edge()实现了从parent向child添加一条edge的操作。
edge通过sruct dag_edge表示,其用于指向child这个node和特定data。
函数dag_add_edge()两个参数parent和child代表两个node,完成添加后会有一条边由parent流向child(parent –> child)。
首先,函数函数dag_add_edge()中判断,节点parent已有edge指向child,有则退出函数。否则先将该node从dag header中移除,再将child加入到parent的edges所在的动态数组中,并对child->parent_count++,即依赖计数增加1。
根据需要调度指令间的依赖关系,依次调用函数dag_add_edge()后,依然留在dag header中的指令,即为没有依赖项的,准备好了的可调度指令。
/** * Adds a directed edge from the parent node to the child. * * Both nodes should have been initialized with dag_init_node(). The edge * list may contain multiple edges to the same child with different data. */ void dag_add_edge(struct dag_node *parent, struct dag_node *child, void *data) { util_dynarray_foreach(&parent->edges, struct dag_edge, edge) { if (edge->child == child && edge->data == data) return; } /* Remove the child as a DAG head. */ list_delinit(&child->link); struct dag_edge edge = { .child = child, .data = data, }; util_dynarray_append(&parent->edges, struct dag_edge, edge); child->parent_count++; }
依据依赖关系构建完DAG后,依旧留在dag->heads中的node就构成了active list。
在开始指令调度前,需要计算出DAG中node的Length of the longest (latency) chain,以下简称latency。
latency的计算是自底向上的遍历各个node,具体计算单个node的delay是通过函数dag_traverse_bottom_up()的回调函数接口实现的,这个需要根据实际的指令集来计算。
struct dag_traverse_bottom_up_state { struct set *seen; void *data; }; static void dag_traverse_bottom_up_node(struct dag_node *node, void (*cb)(struct dag_node *node, void *data), struct dag_traverse_bottom_up_state *state) { if (_mesa_set_search(state->seen, node)) return; util_dynarray_foreach(&node->edges, struct dag_edge, edge) { dag_traverse_bottom_up_node(edge->child, cb, state); } cb(node, state->data); _mesa_set_add(state->seen, node); } /** * Walks the DAG from leaves to the root, ensuring that each node is only seen * once its children have been, and each node is only traversed once. */ void dag_traverse_bottom_up(struct dag *dag, void (*cb)(struct dag_node *node, void *data), void *data) { struct dag_traverse_bottom_up_state state = { .seen = _mesa_pointer_set_create(NULL), .data = data, }; list_for_each_entry(struct dag_node, node, &dag->heads, link) { dag_traverse_bottom_up_node(node, cb, &state); } ralloc_free(state.seen); }
完成latency的计算,就要进行指令的调度了。
当前可选的指令处在dag->heads的列表中,即为active list。
从active list中挑出一条指令的规则,一般会通过一个clock来计数当前的执行周期,并根据实际的目标处理器的特点来构建。
当我们从active list中挑出一条具体的指令后,需要将该指令从active list中移除,并且更新active list。因为当一条指令被调度后,对其依赖的指令,如果没有其他依赖的指令没有被调度,那么该依赖的指令需要加入到active list中,该操作由函数dag_prune_head()完成。
函数dag_prune_head()首先将被调度的node从dag->heads中移除;然后依次遍历该node的每条edge所指向的node,遍历到这些nodes后,将他们的依赖计数进行更新(减一),如果更新后node的依赖计数等于零,说明该node的依赖项均已被调度,该node会被加入到active list(dag->heads)中。
/* Removes a single edge from the graph, promoting the child to a DAG head. * * Note that calling this other than through dag_prune_head() means that you * need to be careful when iterating the edges of remaining nodes for NULL * children. */ void dag_remove_edge(struct dag *dag, struct dag_edge *edge) { if (!edge->child) return; struct dag_node *child = edge->child; child->parent_count--; if (child->parent_count == 0) list_addtail(&child->link, &dag->heads); edge->child = NULL; edge->data = NULL; } /** * Removes a DAG head from the graph, and moves any new dag heads into the * heads list. */ void dag_prune_head(struct dag *dag, struct dag_node *node) { assert(!node->parent_count); list_delinit(&node->link); util_dynarray_foreach(&node->edges, struct dag_edge, edge) { dag_remove_edge(dag, edge); } }
本文主要是通过对《Engineering a compiler》翻译整理而来。
本文中参考的代码若无特别指出均是来源于mesa中的源文件 src\util\dag.c 和 src\util\dag.c。
参考资料:
Advanced compiler design and implementation / Steve Muchnick.