《CSAPP》實驗一:位操做

《CSAPP》號稱程序員聖經,雖然中文譯名爲《深刻理解計算機系統》,但其實沒那麼「深」,只是覆蓋面很廣,通常用做計算機專業大一導論課的教科書。早就聽聞書上配套的實驗十分經典,此次重溫新版(第三版),打算把全部的實驗都作一下,也寫個系列博文,好記錄實驗過程。實驗能夠在書本配套網站CSAPP: Lab Assignments下載,這篇從第一個實驗 —— 位操做開始。html

概述

本實驗是第二章《信息的表示與處理》的配套實驗,要求使用一個高度限制的C語言子集實現一些特定的邏輯,整數,浮點數的函數。延用第一章的說法,信息就是位加上下文,計算機系統中全部的信息都是由一串比特(或者說一串二進制數字)表示的,第二章就講了C語言中整數與浮點數的編碼方式,即典型地,計算機是如何用一串比特來表示整數與浮點數的:程序員

  • 無符號整數:直接二進制編碼
  • 有符號整數:二進制補碼,最高位爲負權
  • 浮點數:IEEE 754

一樣從內存裏取出4個字節 \(0x80000000\) ,把它當無符號整數看,就是 \(2147483648\);把它當有符號整數看,就是 \(-2147483648\);把它當單精度浮點數看,就是 \(-0\)。所謂上下文,就是解讀這串比特的方式,橫當作嶺側成峯。值得注意的是,儘管在幾乎全部系統上,C語言整數與浮點數都是這麼編碼的,但C語言標準自己並無這樣規定,不知道有生之年能不能趕上非主流的編碼方式。
若是沒有徹底掌握這些數字的編碼方式以及C語言的位操做,是必定沒法完成實驗一的。實驗一好就好在會讓你反覆回憶這些基礎知識,深刻細節之中,作完實驗後想忘都忘不了:)express

前提

儘管有C語言有標準,但Undefined Behavior仍是太多,尤爲是深刻底層進行位操做的狀況下,所以實驗預設: 有符號整數使用32位二進制補碼編碼; 右移操做爲算術位移,高位補符號位。實驗還要求:不能使用宏;整數操做不能使用大於0xFF的常量。下面就逐個函數記錄實驗過程了。app

bitAnd

~|實現&,有公式很簡單,但記不住,用韋恩圖輔助思考:全集表示全部位都爲1,xy分別表示特定位置爲1的子集,想象一下~|&的韋恩圖,一會兒就推出公式來了。less

/*
 * bitAnd - x&y using only ~ and |
 *   Example: bitAnd(6, 5) = 4
 *   Legal ops: ~ |
 *   Max ops: 8
 *   Rating: 1
 */
int bitAnd(int x, int y) {
    return ~(~x | ~y);
}

getByte

x右移 n * 8 位,取最後一個字節便可,利用了n * 8 == n << 3函數

/*
 * getByte - Extract byte n from word x
 *   Bytes numbered from 0 (LSB) to 3 (MSB)
 *   Examples: getByte(0x12345678,1) = 0x56
 *   Legal ops: ! ~ & ^ | + << >>
 *   Max ops: 6
 *   Rating: 2
 */
int getByte(int x, int n) {
    return (x >> (n << 3)) & 0xFF;
}

logicalShift

實驗預設了右移爲算術位移,那麼對x右移n位再用掩碼將高位補的n位置0便可。網站

/*
 * logicalShift - shift x to the right by n, using a logical shift
 *   Can assume that 0 <= n <= 31
 *   Examples: logicalShift(0x87654321,4) = 0x08765432
 *   Legal ops: ! ~ & ^ | + << >>
 *   Max ops: 20
 *   Rating: 3
 */
int logicalShift(int x, int n) {
    int mask = ~(((1 << 31) >> n) << 1);
    return (x >> n) & mask;
}

bitCount

這題想了好久,正常的想法是將x一位一位地右移,用掩碼1取最低位,再求和,然而操做符數量超標:D 而後想到,用x & 1去檢查x最後一位是不是1比較虧,能夠用x & 0x00010001,這樣能夠一次檢查兩位,最後將先後16位的結果彙總便可,然而操做符數量仍是超標:D最終將x分了8組,x & 0x11111111,每次檢查8位,用了38個操做符,終於達標。這是全部題目中用的操做符數量最多的一題了。ui

/*
 * bitCount - returns count of number of 1's in word
 *   Examples: bitCount(5) = 2, bitCount(7) = 3
 *   Legal ops: ! ~ & ^ | + << >>
 *   Max ops: 40
 *   Rating: 4
 */
int bitCount(int x) {
    int mask = 0x11 + (0x11 << 8) + (0x11 << 16) + (0x11 << 24);

    int count = (x & mask) + ((x >> 1) & mask) +
        ((x >> 2) & mask) + ((x >> 3) & mask);

    return (count & 7) + ((count >> 4) & 7) + ((count >> 8) & 7) +
        ((count >> 12) & 7) + ((count >> 16) & 7) + ((count >> 20) & 7) +
        ((count >> 24) & 7) + ((count >> 28) & 7);
}

bang

一開始想在0上面做文章,畢竟只有bang(0) = 1,但此路不通。|操做,二分法,逐漸把高位的1收集到低位,如x = x | (x >> 16),若是高位的16位有1的話,就會被收集到低位的16位上,依此二分,收集到最後一位,恰好12個操做符。編碼

/*
 * bang - Compute !x without using !
 *   Examples: bang(3) = 0, bang(0) = 1
 *   Legal ops: ~ & ^ | + << >>
 *   Max ops: 12`
 *   Rating: 4
 */
int bang(int x) {
    x = x | (x >> 16);
    x = x | (x >> 8);
    x = x | (x >> 4);
    x = x | (x >> 2);
    x = x | (x >> 1);
    return ~x & 1;
}

tmin

最簡單的一題,要熟悉二進制補碼。spa

/*
 * tmin - return minimum two's complement integer
 *   Legal ops: ! ~ & ^ | + << >>
 *   Max ops: 4
 *   Rating: 1
 */
int tmin(void) {
    return 1 << 31;
}

fitsBits

x非負,考慮到n位二進制補碼能表示的最大非負數爲 $0b0111...111 $ (共n-1個1),用掩碼將x低位的n-1位置0,檢查高位的32 - (n - 1)位是否爲0便可。若x爲負,先將其轉爲非負數~x,編碼~x必需的位數與編碼x的是相同的。

/*
 * fitsBits - return 1 if x can be represented as an
 *  n-bit, two's complement integer.
 *   1 <= n <= 32
 *   Examples: fitsBits(5,3) = 0, fitsBits(-4,3) = 1
 *   Legal ops: ! ~ & ^ | + << >>
 *   Max ops: 15
 *   Rating: 2
 */
int fitsBits(int x, int n) {
    int minusOne = ~0;
    int mask = minusOne << (n + minusOne);
    return !((x ^ (x >> 31)) & mask);
}

divpwr2

x >> n即爲\(\lfloor x / 2^n \rfloor\),結果是向下取整的,但題目要求向0取整,若x非負向下取整便是向0取整沒有問題,若x爲負,須要向x加上一個偏移值\(2^n - 1\),使得x >> n向上取整。

/*
 * divpwr2 - Compute x/(2^n), for 0 <= n <= 30
 *  Round toward zero
 *   Examples: divpwr2(15,1) = 7, divpwr2(-33,4) = -2
 *   Legal ops: ! ~ & ^ | + << >>
 *   Max ops: 15
 *   Rating: 2
 */
int divpwr2(int x, int n) {
    int signBit = (x >> 31) & 1;
    int bias = (signBit << n) + (~signBit + 1);
    return (x + bias) >> n;
}

negate

n位二進制補碼的值域是\([-2^{n-1},\ 2^{n-1} - 1]\),並不關於0對稱,所以當x爲最小值時-x是它本身。

/*
 * negate - return -x
 *   Example: negate(1) = -1.
 *   Legal ops: ! ~ & ^ | + << >>
 *   Max ops: 5
 *   Rating: 2
 */
int negate(int x) {
  return ~x + 1;
}

isPositive

正數的符號位爲0,0的符號位也是0,是特殊狀況。

/*
 * isPositive - return 1 if x > 0, return 0 otherwise
 *   Example: isPositive(-1) = 0.
 *   Legal ops: ! ~ & ^ | + << >>
 *   Max ops: 8
 *   Rating: 3
 */
int isPositive(int x) {
    return (!!x) & (!(x >> 31));
}

isLessOrEqual

isLessOrEqual等價於!isGreater,實現isGreater簡單點:若x y異號,則x必須非負y必須爲負;若x y 同號,x - y不會溢出,必有x - y > 0,即x - y - 1 >= 0,即x + ~y >= 0,檢查x + ~y的符號位便可。

/*
 * isLessOrEqual - if x <= y  then return 1, else return 0
 *   Example: isLessOrEqual(4,5) = 1.
 *   Legal ops: ! ~ & ^ | + << >>
 *   Max ops: 24
 *   Rating: 3
 */
int isLessOrEqual(int x, int y) {
    int xSign = x >> 31;
    int ySign = y >> 31;

    int hasSameSign = !(xSign ^ ySign);
    int diffSign = (x + ~y) >> 31;

    int isXPosYNeg = (!xSign) & ySign;
    int isGreater = isXPosYNeg | (hasSameSign & !diffSign);

    return !isGreater;
}

ilog2

這道題容許90個操做符,是全部題目對操做符數量最寬鬆的了。ilog2的實質是求x最高位的1的索引,若x高位的16位有1,則不用管低位的16位;若x高位的8位有1,則不用管低位的24位,依次類推。實現起來仍是十分巧妙的:)

/*
 * ilog2 - return floor(log base 2 of x), where x > 0
 *   Example: ilog2(16) = 4
 *   Legal ops: ! ~ & ^ | + << >>
 *   Max ops: 90
 *   Rating: 4
 */
int ilog2(int x) {
    int high16, high8, high4, high2, high1;

    high16 = (!!(x >> 16)) << 4;
    x = x >> high16;

    high8 = (!!(x >> 8)) << 3;
    x = x >> high8;

    high4 = (!!(x >> 4) << 2);
    x = x >> high4;

    high2 = (!!(x >> 2) << 1);
    x = x >> high2;

    high1 = !!(x >> 1);
    return high1 + high2 + high4 + high8 + high16;
}

float_neg

終於到浮點數了,浮點數的題對操做符要求寬鬆一點,還能夠用循環跟判斷語句。第一題,只要對IEEE754熟悉就好了。

/*
 * float_neg - Return bit-level equivalent of expression -f for
 *   floating point argument f.
 *   Both the argument and result are passed as unsigned int's, but
 *   they are to be interpreted as the bit-level representations of
 *   single-precision floating point values.
 *   When argument is NaN, return argument.
 *   Legal ops: Any integer/unsigned operations incl. ||, &&. also if, while
 *   Max ops: 10
 *   Rating: 2
 */
unsigned float_neg(unsigned uf) {
    int isNaN = (((uf >> 23) & 0xFF) == 0xFF) && (uf << 9);
    return isNaN ? uf : ((1 << 31) ^ uf);
}

float_i2f

沒什麼技巧,十分暴力。從符號位,階碼,尾數,舍入,一個一個來。注意,float(x)是向偶數取整的。

/*
 * float_i2f - Return bit-level equivalent of expression (float) x
 *   Result is returned as unsigned int, but
 *   it is to be interpreted as the bit-level representation of a
 *   single-precision floating point values.
 *   Legal ops: Any integer/unsigned operations incl. ||, &&. also if, while
 *   Max ops: 30
 *   Rating: 4
 */
unsigned float_i2f(int x) {
    unsigned sign = x & (1 << 31);
    unsigned exp = 0;
    unsigned frac = 0;
    unsigned round = 0;

    unsigned absX = sign ? (~x + 1) : x;
    unsigned tmp = absX;
    while ((tmp = tmp >> 1))
        ++exp;

    frac = absX << (31 - exp) << 1;
    round = frac << 23 >> 23;
    frac = frac >> 9;

    if (round > 0x100) round = 1;
    else if (round < 0x100) round = 0;
    else round = frac & 1;

    return x ? (sign | ((exp + 0x7F) << 23) | frac) + round : 0;
}

float_twice

仍是很暴力,按照浮點數分類一個一個來:特殊值,直接返回;規範化的浮點數,階碼加1;非規範化的,左移一位並保持符號位不變。

/*
 * float_twice - Return bit-level equivalent of expression 2*f for
 *   floating point argument f.
 *   Both the argument and result are passed as unsigned int's, but
 *   they are to be interpreted as the bit-level representation of
 *   single-precision floating point values.
 *   When argument is NaN, return argument
 *   Legal ops: Any integer/unsigned operations incl. ||, &&. also if, while
 *   Max ops: 30
 *   Rating: 4
 */
unsigned float_twice(unsigned uf) {
    unsigned sign = 1 << 31;
    unsigned isNormalized = uf << 1 >> 24;
    unsigned isSpecial = isNormalized == 0xFF;

    if (isSpecial || uf == 0 || uf == sign)
        return uf;

    if (isNormalized)
        return uf + (1 << 23);

    return (uf << 1) | (uf & sign);
}
相關文章
相關標籤/搜索