有关primitive operation的参考说明

     这几天收到了一些有关primitive operation计算的问题,问题主要集中在如何判定
    一个操作是否属于primitive operation(以下简称PO),以及与方法调用有关的PO计
    算上.PO作为算法评估的一个基本概念,从本质上讲就是简单的操作指令,注意这里提
    到操作指令是指机器能够直接接受的操作指令,大家可以把它与汇编语言作类比,但
    从认识角度上来讲,PO比汇编稍微高级一点。以下将给出我个人对PO的一些理解,
    仅供参考,希望对大家理解PO有所帮助。

    另外对于同学所寄来的问题,我不作个别回答,希望大家能够在这篇参考说明
    中找到所需的答案。

    在REM的PPT中提到PO由以下几类操作

Assigning a value to a variable
    任何赋值语句都是PO,相当于汇编 mov A B

    EG: a = 1; // 1 PO

Calling a method
    调用一个方法:call methodAddress(将返回地址压栈)
    但是,这里仅仅指调用方法,方法内部的有效运算还得另外算PO数。
    由此,大家可以看到调用方法越多,是会产生越多的执行开销(overhead)。

    这里有一个简单的例子:
Program 1:
    statement1; // 1 PO
    statement2; // 1 PO
    // total 2 PO

Program 2:
    function1(); // 1 PO
    // total 4 PO

function1(){
    statement1; // 1 PO
    statement1; // 1 PO
    // return // 1 more PO, 即便没有写,隐式也会有一个 return
}

    所以不要认为把复杂的算法包装在方法中就能降低计算复杂度,
    这样做只能帮倒忙!!!


Performing an arithmetic operation
    任何一次计算都要被算成一次 PO, 因为CPU一次时钟循环只能处理一个运算指令,
    有时CPU的时钟循环连一个运算都完不成(比如说乘法和除法)。
    add A B, sub A B, mul ...etc

    EG: a + b * ( c - d ); // 3 PO

Comparing two  numbers
    比较运算: test A B, ...etc
    显然每一次值比较,都要算一个PO

    但或许大家也能举出一个“反例”
    int a[] = {1};
    if(a[0] == 3) {...} // 1 + 1 PO,注意这里取a[0]值的时候还计一次PO

    大家也可以想想看,下面例子中Program 3的PO总值是多少?

Progam 3:
    int i; // 注意声明一个变量是不计 PO 的。
    if(getValue() <= 5) {
        i--;
    }

function2(){
    return 4;
}

Indexing into an array
Following an object reference
    以上两种情况,在底层的语言上来看是一样,如果用C语言来描述就是一个指针取值
    的过程:
    A[5] ::= *(a + 5)
    // 虽然看似有两个操作,但在汇编层面上却只有一条取值指令。
    B.field ::= *(B + offsetOf(field, B))
    // 做的好的编译器,会把field在B中的offset算出来,把它当作常量编入指令,
    // 所以姑且也能算作一个 PO

Returning from a method   
    从一个方法返回:ret(将调用call时压栈的地址弹出,
    并将当前指令位置指向该值)

    即便源代码中方法段中没有写return,编译器也会主动地位为他加上,否则程序的
    执行流就无法返回到原先 call 这个函数的位置。

    一个比较有意思的意思就是
    function4() {;} // implicit return
    虽然这个函数什么也没做,但一调用,就耗了 2 PO。

特殊说明:
    最差估计
        如果一个程序有多个分支(if),那么对这个程序的算法复杂度计算进行最差
    估计的话,就要取 PO 数最大的那条路径(以下称critical path)。

    EG:
        // a[0] 个 branch
        if(boolval_1_1){
        }
        else if(boolval_1_2){
        }
        ...
        else if(boolval_1_a_0s)
        else{
        }
        // a[1] 个 branch
        ...
        ...
        // a[m] 个 branch
        if(boolval_m_0){
        }
        ...
        else{
        }

        那么,一共会有a[0]*a[1]*...*a[m]条path,大家都学过排列组合,结果这么
    来的,就不解释了。
        在计算 O(f(n))时,就要取这些path中 PO 数最大的 path,
        O(f(n)) ::= O( Big-Oh of critical path )
        在计算 Ω(f(n))时,就要取这些path中 PO 数最下的 path,
        Ω(f(n)) ::= Ω( Big-Omega of fastest path )
        在计算 Θ(f(n))时,就要采用均摊分析了,
        这个属于高级话题,这里就不提了。

    对于循环来说,问题相对比较简单,大家只要对循环执行次数估计正确就可以了。
        O(f(n))时,取循环最大次数。
        Ω(f(n))时,取循环最小次数。
        Θ(f(n))时,取循环平均次数。

补充解释:
    For的header:
        for i = 1 to n - 1 do // 1 + 2n + 2(n - 1) operations
        1  -- i = 1 赋值
        2n -- n * 2 循环测试会被执行 n 次,
        每次执行 i <= n - 1,如果是true,则继续循环。
        2(n - 1) -- 对 i 值的更新(i++)会被执行 n - 1 次,
        一共会有 n 次,程序流会回到 for header,
        第一次是把 1 赋给 i,剩余 n - 1 次更新 i。(i++)

        所以把REM的式子写成 (1*1 + 2*(n -1)) + 2n,或许更容易理解。
   
    new的本质:
        java中 new 的操作,实际上是一个复合操作:
        new A ::= malloc(sizeof(A)); // 分配足够的内存
                  A()                // 调用构造器

        正是由于这种复杂性(我们无法得知JAVA类库怎么实现),
        所以我们目前可以订一个约定:
        任何 new 和库函数操作,就抵 3 OP。

        // 在Practical 1中,本约定不作数,从Practical 2开始生效。
   
END.

转载于:https://www.cnblogs.com/ymhtp/archive/2005/10/16/256113.html

你可能感兴趣的:(有关primitive operation的参考说明)