作者: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. 1.构建DAG
  2. 2.计算 Latency
  3. 3.指令调度
 
1、构造DAG
DAG构造需要分析数据流依赖关系。
三种依赖关系:
  1. flow dependence or true dependence
  2. antidependence
  3. 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.

list scheduling algorithm 指令调度 —— 笔记的更多相关文章

  1. Thread Pool Engine, and Work-Stealing scheduling algorithm

    http://pages.videotron.com/aminer/threadpool.htm http://pages.videotron.com/aminer/zip/threadpool.zi ...

  2. SystemVerilog Event Scheduling Algorithm

    While simulating System Verilog design and its test-bench including assertions, events has to be dyn ...

  3. Rate Monotonic Scheduling algorithm

    这篇文章写得不错 http://barrgroup.com/embedded-systems/How-To/RMA-Rate-Monotonic-Algorithm 另外rtems的官方文档也有类似说 ...

  4. 《Algorithm算法》笔记:元素排序(2)——希尔排序

    <Algorithm算法>笔记:元素排序(2)——希尔排序 Algorithm算法笔记元素排序2希尔排序 希尔排序思想 为什么是插入排序 h的确定方法 希尔排序的特点 代码 有关排序的介绍 ...

  5. 操作系统学习笔记(五)--CPU调度

    由于第四章线程的介绍没有上传视频,故之后看书来补. 最近开始学习操作系统原理这门课程,特将学习笔记整理成技术博客的形式发表,希望能给大家的操作系统学习带来帮助.同时盼望大家能对文章评论,大家一起多多交 ...

  6. 操作系统概念学习笔记 10 CPU调度

    操作系统概念学习笔记 10 CPU调度 多道程序操作系统的基础.通过在进程之间切换CPU.操作系统能够提高计算机的吞吐率. 对于单处理器系统.每次仅仅同意一个进程执行:不论什么其它进程必须等待,直到C ...

  7. 【GCC编译器】Swing Modulo Scheduling

    1. SMS 在 GCC 中的实现 1.1. 一些基本概念 (1)软流水(Software pipelining )是一种通过重叠不同迭代的指令,使其并行执行,从而改进循环中指令调度的技术.关键思想是 ...

  8. Least slack time scheduling

    This algorithm is also known as least laxity first. 词语解释:Laxity 松懈的:马虎的:不严格的,Least-Laxity-First 松弛程度 ...

  9. 【传智播客】Libevent学习笔记(四):事件event

    目录 00. 目录 01. 事件概述 02. 创建事件 03. 事件的标志 04. 事件持久性 05. 超时事件 06. 信号事件 07. 设置不使用堆分配的事件 08. 事件的未决和非未决 09. ...

随机推荐

  1. 操作系统OS基础

    OS: Operating System 操作系统,通用目的的软件程序 主要功能:硬件驱动 进程管理 内存管理 网络管理 安全管理 文件管理OS分类:服务器OS:CentOS,Ubuntu,Windo ...

  2. springboot入门系列(四):SpringBoot和Mybatis配置多数据源连接多个数据库

    SpringBoot和Mybatis配置多数据源连接多个数据库 目前业界操作数据库的框架一般是 Mybatis,但在很多业务场景下,我们需要在一个工程里配置多个数据源来实现业务逻辑.在SpringBo ...

  3. Python 从入门到精通:一个月就够了

    毫无疑问,Python 是当下最火的编程语言之一.对于许多未曾涉足计算机编程的领域「小白」来说,深入地掌握 Python 看似是一件十分困难的事.其实,只要掌握了科学的学习方法并制定了合理的学习计划, ...

  4. 实战一:建立springcloud基础项目结构

    一,,创建父工程,用于管理项目依赖版本 1,new -> project -> maven 2,修改pom.xml,这里管理了springboot,springcloud,springcl ...

  5. CGANs

    Introducation 1. intruduce the conditional version of GANs, which can be constructed by simply feedi ...

  6. 【5】JMicro其于RSA及AES加密实现安全服务调用

    JMicro是基于Java实现的微服务平台,最近花了两个周未实现服务间安全调用支持. JMicro服务调用分两个部份,分别为内部服务间相互调用和外部客户端通过API网关调用JMicro集群内部服务,前 ...

  7. TypeScript魔法堂:函数类型声明其实很复杂

    前言 江湖有传"动态类型一时爽,代码重构火葬场",由于动态类型语言在开发时不受数据类型的约束,因此非常适合在项目原型阶段和初期进行快速迭代开发使用,这意味着项目未来将通过重写而非重 ...

  8. vue父组件促发子组件中的方法

    实现在父组件中促发子组件里面的方法 子组件: <template> <div> 我是子组件 </div> </template> <script& ...

  9. MONGODB02 - MongoSocketWriteException 异常会迟到,但从不缺席

    接上一个<MONGODB01 - Prematurely reached end of stream 错误定位及修复>处理完成之后,又报错了,场景也是一段时间不访问MongoDB,突然访问 ...

  10. 《Clojure编程》笔记 第5章 宏

    目录 背景简述 第5章 宏 5.0 术语 5.1 宏到底是什么 5.1.1 宏不是什么 5.1.2 有什么是宏能做而函数不能做的 5.1.3 宏vsRuby的eval 5.2 编写你的第一个宏 5.3 ...