Kevin's Blog

Core of Kevin.

CAS & C++0x

(博客很久没更新了…坑爹的作业…) 本来我这篇博客准备写C++里面有关线程安全的,但是我最近看了一c++0x的一些新的标准和函数使用,感觉有必要提前拿出来分享一下。

CAS

说到CAS,我觉得学习C/C++的人应该不会不会陌生,CAS即为compare&swap,我将它理解为 先比较,后交换 。其实要理解它很容易,拿段代码看看

int compare_and_swap(int* reg, int old_value, int new_value) {
    int old_reg_value = *reg;
    if (old_reg_value == old_value)
        *reg = new_value;
    return old_reg_value;
}

上面的代码不知道大家看懂了没有,其实很简单,指针reg指向的值与old_value进行比较,如果是相同的,那么就将new_value赋给reg指向的对象。这个就是一个用C语言描述的CAS简单的实现。为了更好地放在情境中理解,我们可以实现函数返回值类型为bool的版本:

bool compare_and_swap (int *accum, int *dest, int newval) {
    if ( *accum == *dest ) {
        *dest = newval;
        return true;
    } else {
        *accum = *dest;
        return false;
    }
}

我们将设现在有两个线程同时要对a进行操作,那么无论是哪个线程对a进行操作之前要检查自己“掌握”的a的信息与现在a的信息是否匹配,如果匹配,那么就对a进行操作,然后返回true,如果很可惜,在进行操作之前已经被别的线程抢先一步操作了,那么该线程保留的a的信息和a当前的信息不能匹配,那么就返回false。

目前,CAS的实现版本有多种,但是我这里只列出c++11中的CAS的实现版本,看代码~

template <class T>
bool atomic_exchange_weak( std::atomic<T>* obj,
                           T* excepted, T desired );
template <class T>
bool atomic_compare_exchange_weak( volatile std::atomic<T>* obj,
                                   T* excepted, T desired );

template< class T >
bool atomic_compare_exchange_strong( std::atomic<T>* obj,
                                     T* expected, T desired );
template< class T >
bool atomic_compare_exchange_strong( volatile std::atomic<T>* obj,
                                     T* expected, T desired );

template< class T >
bool atomic_compare_exchange_weak_explicit( std::atomic<T>* obj,
                                        T* expected, T desired,
                                        std::memory_order succ, 
                                        std::memory_order fail );
template< class T >
bool atomic_compare_exchange_weak_explicit( volatile std::atomic<T>* obj,
                                        T* expected, T desired,
                                        std::memory_order succ, 
                                        std::memory_order fail );

template< class T >
bool atomic_compare_exchange_strong_explicit( std::atomic<T>* obj,
                                          T* expected, T desired,
                                          std::memory_order succ, 
                                          std::memory_order fail );
template< class T >
bool atomic_compare_exchange_strong_explicit( volatile std::atomic<T>* obj, 
                                          T* expected, T desired,
                                          std::memory_order succ, 
                                          std::memory_order fail );

上面统计一下共有8个函数,但是我们抛开重载不说,其实只有4个函数,std::atomic_compare_exchange_weak, std::atomic_compare_exchange_strong, atomic_compare_exchange_weak_explicitatomic_compare_exchange_strong_explicit。其实我们看到除了参数不同之外,上面的函数可分为weak和strong两类,按照cppreference.com里面关于这两种函数的解释,值得我们玩味:The weak forms of the functions are allowed to fail spuriously, that is, act as if obj != expected even if they are equal. 。这个是上面的原话,这就牵涉到了CAS的实现上的一个叫ABA的问题。我们在下面的内容中将会详细介绍。上面除了weak和strong的区别之外,我们还看到了参数的不同,主要在与volatile关键字的使用和std::memory_order。我们简单解释一下这两个知识点。volatile关键字通常是用来阻止(伪)编译器对那些它认为变量的值不能被代码本身改变的代码上执行任何优化。在C++中,一个被定义为vplatile的变量是为了显式说明这个变量可能在意想不到的情况下被改变,所以编译器不必去假设这个变量的值。优化器在用到这个变量时都必须重新读取这个变量的值而不是使用保存在寄存器的备份。在C/C++中,volatile关键字的作用有:

  1. 允许访问内存映射设备
  2. 允许在setjmp和longjmp之间使用变量
  3. 允许在信号处理函数中使用sig_atomic_t变量

接下来有个例子比较好地说明volatile变量的特性

int square(volatile int* ptr) {
    return *ptr * *ptr;
}

这段代码的本意是要计算ptr所指向值的平方值,很可惜,并不是每次都能得到期望的结果,因为ptr所指向的参数是被volatile修饰的,编译器就会产生如下代码:

int square(volatile int* ptr) {
    int a, b;
    a = *ptr;
    b = *ptr;
    return a * b;
}

现在明白了为什么我们会有可能得不到我们期望的值了吧?如果想每次都能得到期望的结果,那么原代码就需要进行一些改变~

int square(volatile int* ptr) {
    int a;
    a = *ptr;
    return a * a;
}

好了,我们了解了volatie的用处之后,我们来插播一个关键点std::atomic。std::atomic是一个模板类,其中std::atomic struct obj;是一个模板类型为T的原子对象中封装一个类型为T的值。原子类型对象的主要特点就是从不同的县城访问不会导致数据竞争。因此从不同线程访问某个原子对象是良性 (well-defined) 行为,而通常对于非原子类型而言,并发访问某个对象(如果不做任何同步操作)会导致未定义 (undifined) 行为发生。关于c++中的std::atomic以及相关的并发技术,我将在后续的博客中进行学习探索。 现在我们考虑一个问题就是std::memory_order,这个东西到底是个什么玩意儿?我们看一下它的具体的定义先~

enum memory_order {
    memory_order_relaxed,
    memory_order_consume,
    memory_order_acquire,
    memory_order_release,
    memory_order_acq_rel,
    memory_order_seq_cst
};

这样看来,memory_order就是一个枚举类型。memory_order_relaxed(不指定内存屏障,所以内存操作执行时可能是乱序的),memory_order_acquire(插入一个内存读屏障,保证之前的读操作先一步完成,清空cpu上的”invalidate queues”),memory_order_consume(和memory_order_consume差不多吧),memory_order_release(插入内存写屏障), memory_order_acq_rel(同时插入读写两个屏障),memory_order_seq_cst(和memory_order_acq_rel差不多)。以上的实现如果想要细究的话,就要了解内存屏障的知识了,这个就不在这篇博客里面说了,有兴趣就自行google。

根据上面对于CAS在C++11中的实现已经有了初步的认识了,现在将放入具体的代码情境中看看吧(这个代码是cppreference.com里的)

#include <atomic>

template<class T>
struct node {
    T data;
    node* next;
    node(const T& data) : data(data), next(nullptr) {}
};

template <class T>
class stack {
    std::atomic<node<T>*> head;

public:
    void push(const T& data) {
        node<T>* new_node = new node<T>(data);
        new_node->next = head.load(std::memory_order_relaxed);
        while (!std::atomic_compare_exchange_weak_explicit(
                &head,
                &new_node->next,
                new_node,
                std::memory_order_release,
                std::memory_order_relaxed));
    }
};

int main () {
    stack<int> s;
    s.push(1);
    s.push(2);
    s.push(3);
    return 0;
}

上述的代码要实现的是一个简单的压栈问题,这里面,我们考虑到如果有多个线程同时压栈,那么就会产生这样的问题:A在不知道栈顶已经被改变了的情况下,仍然向原来的栈顶压栈,这样显然是有问题的。所以增加了一个Loop,先询问栈顶是否是本线程所“期望”的,如果是,那么就进行压栈,如果不是,继续询问…这样就可以基本上保证一点,那就是压栈线程安全的。

大家看到这里,如果有C++开发经验的可能会产生疑问,我们之前讨论的CAS的compare是进行指针地址的比较,那如果这个地址被重用了呢?这就要关注一个CAS一个非常重要的问题:ABA问题

ABA IN CAS

首先我来描述一下ABA问题。假设有两个线程p1, p2访问同一个变量v,当p1访问变量a的时候,读到v的值为A;此时线程p1被抢占了,p2继续执行,p2首先将v的值变为B,然后又将变量a从B转换为A。此时p1线程执行,它发现v的值还是A,所以认为v在前后没有发生变化,就继续执。这样看起来貌似没有什么问题,这样,我们看一下下面的这个例子 现在有有个用单向链表实现的一个stack,里面有两个元素 A,B,C,其中A是栈顶元素,如下图所示

Stack with A, B and C

我们现在假设有两个进程p1, p2,其中p1需要将栈顶元素变为B,我们要执行的是

compare_and_set(stack_top,A, B);

但是很不幸,我们p1被p2抢占了,这个时候p2要做什么呢?p2要删除B(不要问我为什么,p2就是来捣蛋的,“不给糖就捣蛋~~”),等到p2结束的时候,我们看到现在A,B,C是这个状态:AC在栈中,B已经被删除了,如下图所示

Stack with A and C but not B

好了,现在到p1继续执行了,先比较栈顶元素是否是A,对了,是A,那么compare_and_set指令执行正确了。然后它将栈顶设为原栈顶元素的下一个元素(B),但是B已经被DELETE了,然后栈顶元素会被赋予一个自由的内存空间,因为这个空间是没有被定义的,所以程序很容易就crash了。

这里有一个非常经典的现实场景来说明这个问题:

你拿着一个装满钱的手提箱在飞机场,此时过来了一个火辣性感的美女,然后她很暖昧地挑逗着你,并趁你不注意的时候,把用一个一模一样的手提箱和你那装满钱的箱子调了个包,然后就离开了,你看到你的手提箱还在那,于是就提着手提箱去赶飞机去了。

好了,现在就需要来解决这个ABA问题了,该怎么解决呢?

Wikipedia上面提供了一个双保险的方法:使用double-CAS来解决这个问题,也就是在要比较的值后面家一个版本计数器,当进行比较的时候,只有两个部分都满足才能进行下一步的set的操作,set的时候不仅是要赋予新的值,后面的版本计数器要相应地增加1。这就意味着如果ABA情况发生,即使是操作的值是符合的,但是版本计数器却不相同,这样compare的过程就会不会返回正确的信息。

C++ 中的 String

最近心血来潮,想自己去实现一个小型的C++标准库(基础捉急啊),本身C++标准库已经相当强大了,当然,它也有一些缺陷,但是它的设计 和实现都是值得我们花时间去琢磨的。 插一句,我身边的很多同学都不明白为什么我要重复造轮子,可是,如果我们没有造过轮子,怎么知道轮子是怎么运作的呢?并且,想问一句, 真的这些轮子我们都了解深刻了吗?所以我觉得想要真正了解,必须要自己实践才行,“纸上得来终觉浅,绝知此事要躬行”

我首先是对C++里面的string下手了。

首先,我们需要了解C++中的string的实现的机制,在 Effective STL 这本书中的条款15那一章讲到,string有多种实现方法。这样就会在我们日常代码设计和实现的时候导致一些不确定的因素,在那本书上就提到了一个不起眼的小问题:一个string对象的大小是多少?,我们会直接想到,sizeof(string)呗,有啥问题?在我的电脑上实验的情况如下:
首先是测试代码:

#include <iostream>
#include <string>
using namespace std;
int main() {
    string s1 = "abcde";
    string s2 = s1;
    cout << "Size of s1: ” << sizeof(s1) << endl;
    cout << "Size of s2: " << sizeof(s2) << endl;
    return 0;
}

运行的结果是:

Size of s1: 8
Size of s2: 8

运行的结果其实就是和一个char*的大小一样,当然,这是在我的电脑上实验的。Effective STL 提到了有些编译器实现的string对象的大小是char*的7倍,我们姑且相信这个结论,那么就有一个现象出现了,那就是我们之前提到的,实现string有多种方法。那么主要有几种方法呢?在 Effective STL 里面提到了四种实现方法。

我们先看四种实现(摘自Effective STL):

实现A

String Implementation A

我们看上图,每个string对象包含这些内容:

  1. 配置器的拷贝(Allocator)
  2. 字符串的大小(Size)
  3. 容量(Capacity)
  4. 一个指向动态分配的缓冲区的指针,这个缓冲区包括引用计数和字符串值。

在这里,如果使用默认的配置器,那么这个string的对象的大小是指针大小的4倍。但是如果配置器的大小发生变化,那么string的大小亦会发生变化。

实现B

String Implementation B

现在我们将注意力放在B实现上面,B实现的string指向的对象包含以下元素:

  1. 字符串大小(Size)
  2. 容量(Capative)
  3. 引用计数(RefCnt)
  4. 指向容纳字符串值的动态分配的缓冲区的指针(Pointer)

这里,我们发现,有一个很大的区域(至少图上画得很大>_<)。是的,这是用来干嘛的呢?作者说是因为string指向的对象包含在多线程中与并发控制相关的一些附加数据。好吧,这个是用于存放用于并发控制的数据。

实现C

String Implementation C

在这个实现里面,string的对象就是一个指针,这个指针指向了“一个包含所有与string相关的东西的动态分配缓冲器:它的大小、容量、引用计数和值。”(原文)。这里面有个很显著的区别就是没有了配置器。

实现D

String Implementation D

实现D的string对象是比较大的,在采用默认配置器的情况下足足有一个指针大小的7倍(释疑了)。这个实现跟上面三种实现的一个重要的区别是没有使用引用计数,多了一个缓冲区,这个缓冲区最多可以容纳拥有15个字符的字符串。当需要表示超过15个字符的字符串的时候,这个缓冲区的一部分将会被用作指向动态分配的内存的指针,而字符串会被放在那块内存中。

其实我们可以看到,以上的四种实现方法是个有特点,例如,从共享性来分析。首先,我们可以排除D,因为D没有引入引用计数这一元素,所以对象本身包括对象所指向的内存空间均没有设置为别的对象所引用。然后我们看到,A, B, C中,A的引用计数单纯地是用来表达对象所指的内存的引用次数。而在B和C的实现中,对象本身就包含有引用计数这一成员。并且因为B和C能够共享字符串的大小和容量,而C的实现是唯一一个共享配置器的实现。

现在我们看四种方式实现的string对象的大小,以例子来讲

string s("hello");

这个例子中,s是一个大小为5的字符串,那么在A的实现中,我们之前提到,string的对象的大小是指针大小的4倍,那么A实现中这个string对象s的最小分配的大小就是32,容量是31,因为最后地32个字符会被保留为null。实现B因为没有最小分配,在 Effective STL 上面说容量是7,因为没有做这个实验,所以我不敢确认。实现C因为没有保留尾部的空间,所以它的最小分配的大小是16,容量也是16 。实现D的最小缓冲区大小也是16,包括尾部null的空间。

上面说了这么多,无非是想表达一个观点,简约而不简单(一直觉得这个广告词挺好的,设计上也适用)。我们在实践中很多时候是直接用string,而忽视了它的实现的细节,我们会觉得这个设计很好用,但是我们却没有关注到这个设计背后所做的种种工作。闲话不说,接着讲string。

了解了几种实现之后,作为开发者,所用的最多的编译器无非是Visual C++和G++,那么在这两种编译器里面所支持sring的设计优势如何呢?在G++中,std::string的实现是一种 Copy-on-Write 的实现。而在VIsual C++中的实现就是短字符串优化,利用string对象本身的空间来存储短字符串。下面我们先解释一下什么叫做Copy-on-Write。

Copy-on-Write

Copy-on-Write, 顾名思义,就是写时拷贝,它的核心思想就是在多个实体之间共享资源,当某个实体需要修改这个资源的时候,才会为这个实体分配真正的私有的资源。如果大家对linux操作系统熟悉的话,那么这个概念也就不会陌生,因为它应用在linux kernel在fork一个新的进程的时候对进程的地址空间的处理。当fork一个子进程的时候,子进程会和父进程共享一个地址空间,当子进程或者是父进程需要对内存的某个页面进行修改的时候,才会重新分配一个新的页面,将原来的内容拷贝进新的页面,然后将修改的进程的虚拟地址重新映射到新的页面上。

std::string in G++

我们了解了Copy-on-Write的概念之后,想必对string的这种实现应该有个大致的印象了。其实,这里的G++中的std::string的设计就是上面所说的 实现C 。要实现COW,首先必须要有引用计数。当一个string对象初始化的时候,如果是采用类似与 string s(“abcde”); 这种初始化方式,那么引用计数为1,当 string s1(s); 的时候,引用计数需要加一。当需要对string的内容做修改的时候,就需要考虑引用计数了,如果大于1,那么就需要重新申请一份空间然后拷贝原字符串到新的空间,然后对新空间里的字符串进行操作,并且原来string的引用计数需要减1。

std::string in Visual C++

在Visual C++中, string的实现是基于短字符优化(SSO)的方案实现的,其实就是我们上面所说的 实现D ,如果字符串比较短,那么直接存放在对象的buffer里面,如果字符串比较长,那么就会在堆上分配一段空间,然后指向堆上新分配的空间。这里判断字符串长短的阀值通常是15个字节。

简单提了一下C++ 里面关于string的实现,如果想了解更多的实现细节的话,可以阅读 Effective STL ,或者如果时间比较充裕的话,建议阅读源码吧,套用侯捷先生的一句话 “源码之前 了无秘密”

本文参考:

Effective STL —Scott Meyers
c++工程实践 —陈硕

Rubik’s Magic问题解答

最近上算法课,在中大sicily上面有3道魔板题1150.简单魔板1151.魔板,和1515.魔板C。为了方便讲解,我现将1150题目抄录下来:

题目描述

魔板是由8个大小相同的方块组成,分别涂上不同的颜色,用1到8的数字表示其初始状态是

1 2 3 4
8 7 6 5

对模板可进行三种基本操作:

  • A操作(上下行互换)

8 7 6 5
1 2 3 4

  • B操作(每次以行循环右移一个)

4 1 2 3
5 8 7 6

  • C操作(中间四小块顺时针转一格)

1 7 2 4
8 6 3 5

用上述三种基本操作,可将任意一种状态转换为另一种状态。

输入

输入包括多个要求解的魔板,每个魔板用三行描述。
第一行步数N(不超过10的整数),表示最多容许的步数。
第二、第三行表示目标状态,按照魔板的形状,颜色用1到8的表示。
当N等于-1的时候,表示输入结束

输出

对于每一个要求解的魔板,输出一行。
首先是一个整数M,表示你找到解答所需要的步数。接着若干个空格之后,从第一步开始按顺序给出M步操作(每一步是A、B或C),相邻两个操作之间没有任何空格。
注意:如果不能达到,则M输出-1即可。

样例输入

4
5 8 7 6
4 1 2 3
3
8 7 6 5
1 2 3 4
-1

样例输出

2 AB
1 A

这道题如果单纯从解题的要求上看,不是很难。利用广度优先遍历可以解决这个问题。但是需要进行简单的剪枝,那么下面介绍一下具体的算法设计的过程

算法的整体设计

因为要在限制的步数内找到合理的操作使得模板满足输入中的那些形式,所以可以使用广度优先遍历,那么首先,我们对初始的模板(初始操作X)进行三种操作:A, B, C:

第一次

A 操作: X->XA
B 操作: X->XB
C 操作: X->XC

完成三个操作之后得到三个不同的状态 XA, XB, XC,下面分别对这三个状态再次进行A,B,C三种操作

第二次

对XA操作
A 操作:XA->XAA(这个需要忽略,因为两次连续的A操作相当与没有进行操作)
B 操作:XA->XAB
C 操作:XA->XAC

对XB操作
A 操作:XB->XBA(这个需要忽略,XAB和XBA得到的最终状态是一样的)
B 操作:XB->XBB
C 操作:XB->XBC

对XC操作
A 操作:XC->XCA
B 操作:XC->XCB
C 操作:XC->XCC

经过若干次之后,如果给出的目标魔板状态可以达到,那么会得到一个拥有最少操作步骤的解。那么我们程序的整体的算法就解释清楚了,现在 进行具体的数据结构和算法的设计。

数据结构和算法设计

由上面的分析可知,我们需要记录每次进行操作之后的状态和达到这个状态所进行的所有的操作,我设计一个结构体来存储这两种元素

struct node {
    string str;
    string ops;
};

然后我们需要定义三种变换,这三种变换的函数定义如下:

//A operation
string operationA(string str) {
    string temp = str;
    temp[0] = str[4];
    temp[1] = str[5];
    temp[2] = str[6];
    temp[3] = str[7];
    temp[4] = str[0];
    temp[5] = str[1];
    temp[6] = str[2];
    temp[7] = str[3];
    return temp;
}

// B operation
string operationB(string str) {
    string temp = str;
    temp[0] = str[3];
    temp[1] = str[0];
    temp[2] = str[1];
    temp[3] = str[2];
    temp[4] = str[7];
    temp[5] = str[4];
    temp[6] = str[5];
    temp[7] = str[6];
    return temp;
}

// C operation
string operationC(string str) {
    string temp = str;
    temp[1] = str[5];
    temp[2] = str[1];
    temp[6] = str[2];
    temp[5] = str[6];
    return temp;
}

现在,就要进行过程演绎了。首先,为了方便进行判定状态的重复性,我利用一个容器来存储魔板状态

vector<string> states;

然后我需要将每次生成的新的状态的状态信息和操作信息存到一个新的node里面,然后将node放到一个容器中,这里我选择的容器为队列,因为队列有个特性是每次都将最先一个push进去的置为top,所以这样我可以依次将node取出来进行三种操作,并且可以保证每次取的都是最早放进去的。

queue<node> nodes;

查看新的状态(不是目标状态)是否之前已经生成过了,如果没有生成过,那么就将状态和存储状态&操作的节点分别存入states和nodes中

void findAndPush(node& newNode, queue<node>& nodes, vector<string>& states) {
    if (find(states.begin(), states.end(), newNode.str) == states.end()) {
        states.push_back(newNode.str);
        nodes.push(newNode);
    }   
}

下面是主要的处理函数

node process(const node& org, queue<node>& nodes, vector<string>& states) {
    if (org.str == target)
        return org;
    nodes.push(org);
    states.push_back(org.str);

    while (!nodes.empty()) {
        node curNode = nodes.front();
        nodes.pop();

        if (curNode.ops.length() >= N)
            break;
        string changes[3];
        changes[0] = operationA(curNode.str);
        changes[1] = operationB(curNode.str);
        changes[2] = operationC(curNode.str);

        for (int i = 0; i != 3; i++) {
            node newNode;
            newNode.ops = curNode.ops + ops[i];
            newNode.str = changes[i];

            if (target == newNode.str)
                return newNode;
            findAndPush(newNode, nodes, states);
        }
    }
    node notFound;
    notFound.ops = "error";
    return notFound;
}

当然,这个版本实现是比较简单的,并且效率不高,可以优化的地方在于查找相同状态和状态的表示方法,这都需要进一步的改进,在后面的博客中会进行说明。

一道有意思的union问题

一道简单的C++ union的问题

#include <iostream>
using namespace std;

union A {
    int t;
    char s[2];
};

int main() {
    A a;
    a.t = 0;
    a.s[0] = 1;
    a.s[1] = 2;
    cout << a.t << endl;
    return 0;
}

这个问题很简单,就是让我们回答输出的a.t是多少。问题虽然很小,但是却反映了C++中的两个重要的知识点:union和数据类型

C++ 中的union

我们知道,union是一种特殊的class,同时也是一种构造类型的数据结构,其中声明的所有的成员共享同一块内存。因此,一个union的变量的长度就是等于其声明的成员中最长的长度。如下面的例子所示:

#include <iostream>
using namespace std;

union M {
    char a;
    int b;
    double c;
};

int main() {
    M m;
    cout << sizeof(m) << endl;
    return 0;
}

我们可以看到,上面的代码执行之后得到的结果是 8(这里的单位是byte),然后我们将注释掉 ‘double c’, 运行之后得到的结果是 4。实验的结果正如上文所说的一个union的变量的长度就是等于其声明的成员中最长的长度。那么原始的问题中定义的union A的大小是多少呢?由于 char s[2]的大小是2,而int的大小是4,那么显然,在main函数的变量 a 的大小就是4.

整数的表示

如果说这道题只是单纯地进行union的分析,那么它的价值就不会很大了,这道题的另一个知识点就是整数的表示。 大家都知道数据在计算机中是按字节来存储的(1byte = 8bits),而计算机只能识别0和1这两个数。这样,1个字节能表示256种不同的信息(28 = 256)。比如我们要定一个1字节的无符号整数

unsigned char a;

那么a就能表示在[0,255]区间内的数,比如说我现在设定a = 17那么在计算机中,a就表示为00010001。而计算机中无符号数就是按照这个书的本身的二进制码来存储的。但是别急,这只是无符号整数的表示,而下面有符号的整数就不是这么简单了。

有符号整数的表示

从上文我们可以知道无符号数的表示直接以原码的形式展现出来,但是一旦一个数是负数的时候该如何表示呢?如果是书面的去写负数,很简 单,正数前面加一个就可以了,但是我们却不能在计算机中这么做(只能识别1和0的计算机君表示根本不知道这是什么玩意 儿),那么就引入了这样的一种表示方法,那就是最高位作为符号位,如果第一位是0则表示正数,第一位是1就表示负数,这样的话,对于一个 8位的二进制来说,表示数值的只有7位了,那么表示有符号整数的正数范围是[1, 127], 负数的范围就是[-127, -1],当然,还有0,但是 我们看到一个问题,那就是当我们表示-127的时候,二进制表示为11111111,这和无符号数里面的255是一样的,那么具体是表示-1还是 255,就需要我们人为来指定了。而在计算机中,真正用来表示有符号数的是补码。这里我们需要了解:原码,反码,补码。 对于正数,原码/反码/补码都是其原码本身;但是负数的反码是除去符号位之外的所有位取反,而补码是反码加一。 上述的内容大家可能觉得知其然,但是为什么有符号整数需要用补码来表示呢? 我们首先看看如果用原码来表示有符号数有什么不合逻辑的地方,让我们来表示+0和-0。 +0: 00000000 -0: 10000000 反码表示+0和-0 +0: 00000000 -0: 11111111 上述的两种表示中,+0和-0的表示不是一样的,这样就有违背我们数学上关于0的意义。下面,我们看看补码是如何表示+0和-0的 +0: 00000000 -0: 00000000 哈,两种表示是一样的,那么自然我们采取补码来表示有符号整数,其实非负数的表示范围是00000000~01111111,而负数的表示范围是 11111111~10000000,记住了,10000000不是表示0的,是表示-128。所以这样一来,补码表示就比原码和反码表示的范围都大,并且 也解释了为什么计算机中有符号整数的范围是非对称的^–^

程序的结果

根据上述的关于union和有符号整数的分析,我们可以知道程序的输出了。简单地演算一下吧


a.t = 0;    //00000000 00000000 00000000 00000000
a.s[0] = 1; //00000000 00000000 00000000 00000001
a.s[1] = 2; //00000000 00000000 00000010 00000001

结果就是 27 + 20 = 513

ubuntu12.04安装amd显卡驱动

ubuntu12.04安装amd显卡驱动

参考文章, 其中对于一些小问题进行了改善

我的机器的配置是:

  • CPU: i5-4430
  • GPU: HD7850
  • OS: ubuntu12.04_64

根据显卡型号来下载特定的显卡驱动,其实可以用开源的显卡驱动,但是对于AMD来说,闭源的显卡驱动比开源的要好,所以我们就直接在AMD官网上下载就行

卸载已有的驱动(如果没有,可以无视这个步骤)


sudo sh /usr/share/ati/fglrx-uninstall.sh
sudo apt-get remove --purge fglrx fglrx_* fglrx-amdcccle* fglrx-dev*
sudo apt-get remove --purge xorg-driver-fglrx xserver-xorg-video-ati xserver-xorg-video-radeon

更新源


sudo apt-get update

安装打包支持环境


sudo apt-get install dpkg build-essential cdbs dh-make dkms execstack dh-modaliases fakeroot libqtgui4 dpkg-dev

sudo apt-get install build-essential cdbs fakeroot dh-make debhelper debconf libstdc++6 dkms libqtgui4 wget execstack libelfg0 dh-modaliases

安装ia32位库


sudo apt-get install ia32-libs
sudo apt-get install ia32-libs-multiarch:i386 lib32gcc1 libc6-i386

解压下载下来的驱动,然后进入安装目录,执行:


sudo chmod +x amd-driver-installer-catalyst-13-4-x86.x86_64.run

sudo sh ./amd-driver-installer-catalyst-13-4-x86.x86_64.run --buildpkg Ubuntu/raring

这会生成三个文件:fglrx_12.104-0ubuntu1_amd64.deb, fglrx-amdcccle_12.104-0ubuntu1_amd64.deb,fglrx-dev_12.104-0ubuntu1_amd64.deb

安装打包好的deb文件


sudo dpkg -i fglrx*.deb

重启

在上述命令完成之后,重启电脑,发现电脑竟然不能全屏,这让我很是纠结,上网找了N多资料,最终搞定了。 若是遇到非全屏的问题,执行:


sudo amdconfig --set-pcs-val=MCIL,DigitalHDTVDefaultUnderscan,0
sudo reboot

这中间如果出现关于config文件找不到的问题,需要init一下config文件,不过错误提示中会指导你怎么做,所以不必担心。

重启之后发现完全OK啦,好吧,这个问题也就搞定了。