loop_in_codes

低调做技术__欢迎移步我的独立博客 codemaro.com 微博 kevinlynx

#

使用Clang实现C语言编程规范检查

概述

Clang是LLVM编译器工具集的前端部分,也就是涵盖词法分析、语法语义分析的部分。而LLVM是Apple在Mac OS上用于替代GCC工具集的编译器软件集合。Clang支持类C语言的语言,例如C、C++、Objective C。Clang的与众不同在于其模块化的设计,使其不仅实现编译器前端部分,并且包装成库的形式提供给上层应用。使用Clang可以做诸如语法高亮、语法检查、编程规范检查方面的工作,当然也可以作为你自己的编译器前端。

编程规范一般包含编码格式和语义规范两部分。编码格式用于约定代码的排版、符号命名等;而语义规范则用于约定诸如类型匹配、表达式复杂度等,例如不允许对常数做逻辑运算、检查变量使用前是否被赋值等。本文描述的主要是基于语义方面的检查,其经验来自于最近做的一个检查工具,该工具实现了超过130条的规范。这份规范部分规则来自于MISRA C

编程模式

编译器前端部分主要是输出代码对应的抽象语法树(AST)。Clang提供给上层的接口也主要是围绕语法树来做操作。通过google一些Clang的资料,你可能会如我当初一样对该如何正确地使用Clang心存疑惑。我最后使用的方式是基于RecursiveASTVisitor。这是一种类似回调的使用机制,通过提供特定语法树节点的接口,Clang在遍历语法树的时候,在遇到该节点时,就会调用到上层代码。不能说这是最好的方式,但起码它可以工作。基于RecursiveASTVisitor使用Clang,程序主体框架大致为:

// 编写你感兴趣的语法树节点访问接口,例如该例子中提供了函数调用语句和goto语句的节点访问接口
class MyASTVisitor : public RecursiveASTVisitor<MyASTVisitor> {
public:
    bool VisitCallExpr(CallExpr *expr);

    bool VisitGotoStmt(GotoStmt *stmt);
    ...
};

class MyASTConsumer : public ASTConsumer {
public:
    virtual bool HandleTopLevelDecl(DeclGroupRef DR) {
        for (DeclGroupRef::iterator b = DR.begin(), e = DR.end(); b != e; ++b) {
            Visitor.TraverseDecl(*b);
        }
        return true;
    } 
    
private:
    MyASTVisitor Visitor;
};

int main(int argc, char **argv) {
    CompilerInstance inst;
    Rewriter writer;
    inst.createFileManager();
    inst.createSourceManager(inst.getFileManager());
    inst.createPreprocessor();
    inst.createASTContext();
    writer.setSourceMgr(inst.getSourceManager(), inst.getLangOpts());
    ... // 其他初始化CompilerInstance的代码
  
    const FileEntry *fileIn = fileMgr.getFile(argv[1]);
    sourceMgr.createMainFileID(fileIn);
    inst.getDiagnosticClient().BeginSourceFile(inst.getLangOpts(), &inst.getPreprocessor());
    MyASTConsumer consumer(writer);
    ParseAST(inst.getPreprocessor(), &consumer, inst.getASTContext());
    inst.getDiagnosticClient().EndSourceFile();
    return 0;
}

以上代码中,ParseAST为Clang开始分析代码的主入口,其中提供了一个ASTConsumer。每次分析到一个顶层定义时(Top level decl)就会回调MyASTConsumer::HandleTopLevelDecl,该函数的实现里调用MyASTVisitor开始递归访问该节点。这里的decl实际上包含定义。

这里使用Clang的方式来源于Basic source-to-source transformation with Clang

语法树

Clang中视所有代码单元为语句(statement),Clang中使用类Stmt来代表statement。Clang构造出来的语法树,其节点类型就是Stmt。针对不同类型的语句,Clang有对应的Stmt子类,例如GotoStmt。Clang中的表达式也被视为语句,Clang使用Expr类来表示表达式,而Expr本身就派生于Stmt

每个语法树节点都会有一个子节点列表,在Clang中一般可以使用如下语句遍历一个节点的子节点:

for (Stmt::child_iterator it = stmt->child_begin(); it != stmt->child_end(); ++it) {
    Stmt *child = *it;
}

但遗憾的是,无法从一个语法树节点获取其父节点,这将给我们的规范检测工具的实现带来一些麻烦。

TraverseXXXStmt

在自己实现的Visitor中(例如MyASTVisitor),除了可以提供VisitXXXStmt系列接口去访问某类型的语法树节点外,还可以提供TraverseXXXStmt系列接口。Traverse系列的接口包装对应的Visit接口,即他们的关系大致为:

bool TraverseGotoStmt(GotoStmt *s) {
    VisitGotoStmt(s);
    return true;
}

例如对于GotoStmt节点而言,Clang会先调用TraverseGotoStmt,在TraverseGotoStmt的实现中才会调用VisitGotoStmt。利用Traverse和Visit之间的调用关系,我们可以解决一些因为不能访问某节点父节点而出现的问题。例如,我们需要限制逗号表达式的使用,在任何地方一旦检测到逗号表达式的出现,都给予警告,除非这个逗号表达式出现在for语句中,例如:

a = (a = 1, b = 2); /* 违反规范,非法 */
for (a = 1, b = 2; a < 2; ++a) /* 合法 */

逗号表达式对应的访问接口为VisitBinComma,所以我们只需要提供该接口的实现即可:

class MyASTVisitor : public RecursiveASTVisitor<MyASTVisitor> {
public:
    ...
    bool VisitBinComma(BinaryOperator *stmt) {
        /* 报告错误 */
        return true;
    }
    ...
};

(注:BinaryOperator用于表示二目运算表达式,例如a + b,逗号表达式也是二目表达式)

但在循环中出现的逗号表达式也会调用到VisitBinComma。为了有效区分该逗号表达式是否出现在for语句中,我们可以期望获取该逗号表达式的父节点,并检查该父节点是否为for语句。但Clang并没有提供这样的能力,我想很大一部分原因在于臆测语法树(抽象语法树)节点的组织结构(父节点、兄弟节点)本身就不是一个确定的事。

这里的解决办法是通过提供TraverseForStmt,以在进入for语句前得到一个标识:

class MyASTVisitor : public RecursiveASTVisitor<MyASTVisitor> {
public:
    ...
    // 这个函数的实现可以参考RecursiveASTVisitor的默认实现,我们唯一要做的就是在for语句的头那设定一个标志m_inForLine
    bool TraverseForStmt(ForStmt *s) {
        if (!WalkUpFromForStmt(s))
            return false;
        m_inForLine = true;
        for (Stmt::child_range range = s->children(); range; ++range) {
            if (*range == s->getBody())
                m_inForLine = false;
            TraverseStmt(*range);
        }
        return true;
    }

    bool VisitBinComma(BinaryOperator *stmt) {
        if (!m_inForLine) {
            /* 报告错误 */
        }
        return true;
    }
    ...
};

(注:严格来说,我们必须检查逗号表达式是出现在for语句的头中,而不包括for语句循环体)

类型信息

对于表达式(Expr)而言,都有一个类型信息。Clang直接用于表示类型的类是QualType,实际上这个类只是一个接口包装。这些类型信息可以用于很多类型相关的编程规范检查。例如不允许定义超过2级的指针(例如int ***p):

bool MyASTVisitor::VisitVarDecl(VarDecl *decl) { // 当发现变量定义时该接口被调用
    QualType t = decl->getType(); // 取得该变量的类型
    int pdepth = 0;
    // check pointer level
    for ( ; t->isPointerType(); t = t->getPointeeType()) { // 如果是指针类型就获取其指向类型(PointeeType)
        ++pdepth;
    }
    if (pdepth >= 3)
        /* 报告错误 */
}

可以直接调用Expr::getType接口,用于获取指定表达式最终的类型,基于此我们可以检查复杂表达式中的类型转换,例如:

float f = 2.0f;
double d = 1.0;
f = d * f; /* 检查此表达式 */

对以上表达式的检查有很多方法,你可以实现MyASTVisitor::VisitBinaryOperator(只要是二目运算符都会调用),或者MyASTVisitor::VisitBinAssign(赋值运算=调用)。无论哪种方式,我们都可以提供一个递归检查两个表达式类型是否相同的接口:

bool HasDiffType(BinaryOperator *stmt) {
    Expr *lhs = stmt->getLHS()->IgnoreImpCasts(); // 忽略隐式转换
    Expr *rhs = stmt->getRHS()->IgnoreImpCasts();
    if (lhs->getType() == rhs->getType())) {
        if (isa<BinaryOperator>(lhs) && HasDiffType(cast<BinaryOperator>(lhs)))
            return true;
        if (isa<BinaryOperator>(rhs) && HasDiffType(cast<BinaryOperator>(rhs)))
            return true;
        return false;
    }
    return true;
}

(注:此函数只是简单实现,未考虑类型修饰符之类的问题)

该函数获得二目运算表达式的两个子表达式,然后递归检测这两个表达式的类型是否相同。

Expr类提供了更多方便的类型相关的接口,例如判定该表达式是否为常数,是否是布尔表达式,甚至在某些情况下可以直接计算得到值。例如我们可以检查明显的死循环:

while (1) { }

可以使用:

ASTContext &context = inst.GetASTContext();
bool result;
// 假设stmt为WhileStmt
if (stmt->getCond()->EvaluateAsBooleanCondition(result, context)) {
    if (result) 
        /* 死循环 */

符号表

符号表这个概念比较广义,这里我仅指的是用于保存类型和变量信息的表。Clang中没有显示的符号表数据结构,但每一个定义都有一个DeclContextDeclContext用于描述一个定义的上下文环境。有一个特殊的DeclContext被称为translation unit decl,其实也就是全局环境。利用这个translation unit decl,我们可以获取一些全局符号,例如全局变量、全局类型:

// 获取全局作用域里指定名字的符号列表
DeclContext::lookup_result GetGlobalDecl(const std::string &name) {
    ASTContext &context = CompilerInst::getSingleton().GetASTContext();
    DeclContext *tcxt = context.getTranslationUnitDecl();
    IdentifierInfo &id = context.Idents.get(name);
    return tcxt->lookup(DeclarationName(&id));
}

// 可以根据GetGlobalDecl的返回结果,检查该列表里是否有特定的定义,例如函数定义、类型定义等
bool HasSpecDecl(DeclContext::lookup_result ret, Decl::Kind kind) {
    for (size_t i = 0; i < ret.size(); ++i) {
        NamedDecl *decl = ret[i];
        if (decl->getKind() == kind) {
            return true;
        }
    }
    return false;
}

有了以上两个函数,我们要检测全局作用域里是否有名为”var”的变量定义,就可以:

HasSpecDecl(GetGlobalDecl("var"), Decl::Var);

每一个Decl都有对应的DeclContext,要检查相同作用域是否包含相同名字的符号,其处理方式和全局的方式有点不一样:

// 检查在ctx中是否有与decl同名的符号定义
bool HasSymbolInContext(const NamedDecl *decl, const DeclContext *ctx) {
    for (DeclContext::decl_iterator it = ctx->decls_begin(); it != ctx->decls_end(); ++it) {
        Decl *d = *it;
        if (d != decl && isa<NamedDecl>(d) && 
            cast<NamedDecl>(d)->getNameAsString() == decl->getNameAsString())
            return true;
    }
    return false;
}

bool HasSymbolInContext(const NamedDecl *decl) {
    return HasSymbolInContext(decl, decl->getDeclContext());
}

可以看出,这里检查相同作用域的方式是遍历上下文环境中的所有符号,但对于全局作用域却是直接查找。对于DeclContext的详细信息我也不甚明了,只能算凑合使用。实际上,这里使用“作用域”一词并不准确,在C语言中的作用域概念,和这里的context概念在Clang中并非等同。

如果要检查嵌套作用域里不能定义相同名字的变量,例如:

int var;
{
    int var;
}

通过Clang现有的API是无法实现的。因为Clang给上层的语法树结构中,并不包含作用域信息(在Clang的实现中,用于语义分析的类Sema实际上有作用域的处理)。当然,为了实现这个检测,我们可以手动构建作用域信息(通过TraverseCompoundStmt)。

宏的处理属于预处理阶段,并不涵盖在语法分析阶段,所以通过Clang的语法树相关接口是无法处理的。跟宏相关的接口,都是通过Clang的Preprocessor相关接口。Clang为此提供了相应的处理机制,上层需要往Preprocessor对象中添加回调对象,例如:

class MyPPCallback : public PPCallbacks {
public:
    // 处理#include
    virtual void InclusionDirective(SourceLocation HashLoc, const Token &IncludeTok,
        StringRef FileName, bool IsAngled, CharSourceRange FilenameRange,
        const FileEntry *File, StringRef SearchPath, StringRef RelativePath, const Module *Imported) {
    }

    // 处理#define
    virtual void MacroDefined(const Token &MacroNameTok, const MacroInfo *MI) {
    }

    virtual void MacroUndefined(const Token &MacroNameTok, const MacroInfo *MI) {
    } 
}

inst.getPreprocessor().addPPCallbacks(new MyPPCallback());

即,通过实现PPCallbacks中对应的接口,就可以获得处理宏的通知。

Clang使用MacroInfo去表示一个宏。MacroInfo将宏体以一堆token来保存,例如我们要检测宏体中使用###的情况,则只能遍历这些tokens:

// 分别记录#和##在宏体中使用的数量
int hash = 0, hashhash = 0;
for (MacroInfo::tokens_iterator it = MI->tokens_begin(); it != MI->tokens_end(); ++it) {
    const Token &token = *it;
    hash += (token.getKind() == tok::hash ? 1 : 0);
    hashhash += (token.getKind() == tok::hashhash ? 1 : 0);
}

其他

在我们所支持的编程规范中,有些规范是难以支持的,因此我使用了一些蹩脚的方式来实现。

手工解析

在针对函数的参数定义方面,我们支持的规范要求不能定义参数为空的函数,如果该函数没有参数,则必须以void显示标识,例如:

int func(); /* 非法 */
int func(void); /* 合法 */

对于Clang而言,函数定义(或声明)使用的是FunctionDecl,而Clang记录的信息仅包括该函数是否有参数,参数个数是多少,并不记录当其参数个数为0时是否使用void来声明(记录下来没多大意义)。解决这个问题的办法,可以通过SourceLocation获取到对应源代码中的文本内容,然后对此文本内容做手工分析即可。

(注:SourceLocation是Clang中用于表示源代码位置的类,包括行号和列号,所有Stmt都会包含此信息)

通过SourceLocation获取对应源码的内容:

std::pair<FileID, unsigned> locInfo = SM.getDecomposedLoc(loc);
bool invalidTemp = false;
llvm::StringRef file = SM.getBufferData(locInfo.first, &invalidTemp);
if (invalidTemp)
    return false;
// tokenBegin即为loc对应源码内容的起始点
const char *tokenBegin = file.data() + locInfo.second;

要手工分析这些内容实际上还是有点繁杂,为此我们可以直接使用Clang中词法分析相关的组件来完成这件事:

Lexer *lexer = new Lexer(SM.getLocForStartOfFile(locInfo.first), opts, file.begin(), tokenBegin, file.end());
Token tok;
lexer->Lex(tok); // 取得第一个tok,反复调用可以获取一段token流

Diagnostic

Clang中用Diagnostic来进行编译错误的提示。每一个编译错误(警告、建议等)都会有一段文字描述,这些文字描述为了支持多国语言,使用了一种ID的表示方法。总之,对于一个特定的编译错误提示而言,其diagnostic ID是固定的。

在我们的规范中,有些规范检测的代码在Clang中会直接编译出错,例如函数调用传递的参数个数不等于函数定义时的形参个数。当Clang编译出错时,其语法树实际上是不完善的。解决此问题的最简单办法,就是通过diagnostic实现。也就是说,我是通过将我们的特定规范映射到特定的diagnostic,当发生这个特定的编译错误时,就可以认定该规范实际上被检测到。对于简单的情况而言,这样的手段还算奏效。

// `TextDiagnosticPrinter`可以将错误信息打印在控制台上,为了调试方便我从它派生而来
class MyDiagnosticConsumer : public TextDiagnosticPrinter {
public:
    // 当一个错误发生时,会调用此函数,我会在这个函数里通过Info.getID()取得Diagnostic ID,然后对应地取出规范ID
    virtual void HandleDiagnostic(DiagnosticsEngine::Level DiagLevel,
        const Diagnostic &Info) {
        TextDiagnosticPrinter::HandleDiagnostic(DiagLevel, Info);
        // 例如检查三字母词(trigraph)的使用
        if (Info.getID() == 816)
            /* 报告使用了三字母词 */
    }
};

// 初始化时需传入自己定义的diagnostic
inst.createDiagnostics(0, NULL, new MyDiagnosticConsumer(&inst.getDiagnosticOpts()));

该例子代码演示了对三字母词(wiki trigraph)使用限制的规范检测。

全文完。

posted @ 2013-02-12 21:53 Kevin Lynx 阅读(10031) | 评论 (0)编辑 收藏

C++陷阱:构造函数中的多态

C++中主要是通过给函数加上virtual关键字来实现多态。多态可用于改变一个接口的实现,也算是一种嵌入应用层代码到底层的实现手段。就算你用不到C++那些复杂的技术,多态肯定会被用到。

但加上virtual不一定能保证多态成功:

#include <stdio.h>

class Base {
public:
    Base() {
        Init();
    }

    virtual ~Base() {
        Release();
    }

    virtual void Init() {
        printf("Base::Init\n");
    }

    virtual void Release() {
        printf("Base::Release\n");
    }
};

class Derived : public Base {
public:
    virtual void Init() {
        printf("Derived::Init\n");
    }

    virtual void Release() {
        printf("Derived:Release\n");
    }
};

int main()
{
    Base *obj = new Derived();
    delete obj;
    return 0;
}

当在构造函数,包括析构函数中调用virtual函数时,预想中的多态是无法完成的,以上代码输出结果为:

Base::Init
Base::Release

从语言设计角度来看,我个人是不接受这种行为的。我觉得对一门语言而言,几乎所有特性都应该是一致的,不应该或尽量少地出现这种“例外“。如果我构造一个对象,让它以不同的方式被构造,这和改变它的某个行为有什么区别?(从这句话来看,似乎还真有区别)

当然,从语言实现来看,这样的运行结果又似乎是必然的。因为,基类的构造是早于派生类的(作为其一部分),只有当构造完派生类后,其用于支持多态的虚表才会被正确构造。也就是说,在基类中调用虚函数时,既然虚表都为正确构造,自然调用的不会是派生类的虚函数了。析构函数按照析构的顺序来看,也会面临同样的情况。

posted @ 2012-09-17 16:30 Kevin Lynx 阅读(3182) | 评论 (0)编辑 收藏

C++陷阱:virtual析构函数

有一天有个同事在通过vld调试一个内存泄漏问题,折腾了很久然后找到我。我瞥了一眼他的代码,发现问题和我曾经遇到的一模一样:

class Base {
public:
    ~Base();
};

class Derived : public Base {
privated:
    std::vector<int> m_data;    
}; Base *obj = new Derived(); delete obj;

当然,实际代码比这个复杂得多(这也是导致从发现问题到找到问题耗费大量时间的原因)。vld在报内存泄漏时,当然报的位置是new的地方。这个同事检查了这个对象的整个生命周期,确定他正确地释放了这个对象。

问题的关键就在于:Base类的析构函数不是virtual。因为不是virtual,所以在对一个Base类型的指针进行delete时,就不会调用到派生类Derived的析构函数。而派生类里的析构函数会用于析构其内部的子对象,也就是这里的m_data。这样,就造成了内存泄漏。

这其实是一个很低级的失误。但毫不客气地说C++中有很多这种少个关键字或者代码位置不对就会造成另一个结果的例子。事实上,针对这些悲剧也有很多书提出一些准则来让大家去无脑遵守。例如针对这个例子,我就记得曾有书说,只要你觉得你的类会被继承,那么最好给析构函数加上virtual。

posted @ 2012-09-13 17:31 Kevin Lynx 阅读(4478) | 评论 (8)编辑 收藏

C/c++中几种操作位的方法

参考How do you set, clear and toggle a single bit in C?

c/c++中对二进制位的操作包括设置某位为1、清除某位(置为0)、开关某位(toggling a bit)、检查某位是否为1等。这些操作较为常见并且可以作为其他位运算的基础接口,以下罗列几种方法:

传统方法

  • 设置某位为1
number |= 1 << x; // 设置第x位为1
  • 清除某位
number &= ~(1 << x); // 置第x位为0
  • 开关某位
number ^= 1 << x;
  • 检查某位
if (number & (1 << x))

相应地我们可以将其封装起来,简便的方法是使用宏来封装:

#define BIT_SET(a,b) ((a) |= (1<<(b)))
#define BIT_CLEAR(a,b) ((a) &= ~(1<<(b)))
#define BIT_FLIP(a,b) ((a) ^= (1<<(b)))
#define BIT_CHECK(a,b) ((a) & (1<<(b)))

使用位结构操作

这个使用起来简单很多:

struct bits {
    unsigned int a:1;
    unsigned int b:1;
    unsigned int c:1;
};

struct bits mybits;

// set/clear a bit
mybits.b = 1;
mybits.c = 0;

// toggle a bit
mybits.a = !mybits.a;
mybits.b = ~mybits.b;
mybits.c ^= 1;

// check a bit
if (mybits.c)

使用STL的std::bitset

这个方法其实类似于使用位结构,只不过STL包装了这个结构定义,当然还提供了很多便捷的接口:

std::bitset<5> bits;
bits[0] = true;
bits[1] = false;
bits.set(2);
bits.flip(3);
bits.reset(2);

posted @ 2012-09-04 20:29 Kevin Lynx 阅读(4551) | 评论 (2)编辑 收藏

C/c++中的-->运算符

参考What is the name of this operator: “–>”?

c/c++中以下代码是合法的:

#include <stdio.h>
int main()
{
     int x = 10;
     while( x --> 0 ) // x goes to 0
     {
        printf("%d ", x);
     }
}

-->是一个合法的操作符,我打赌自认c/c++熟手的你们都不知道这个操作符。有人称它为goes to操作符,x-->0表示x向0趋近。

其实我在忽悠你们。 并且我相信有很多人对此把戏相当熟悉。没错,-->只是两个操作符恰好遇在了一起,他们是自减运算符--和大于比较运算符>

while (x-- > 0)
    ...

类似的把戏还有:

while (x -- \
\
\
\
> 0) printf("%d ", x);

posted @ 2012-09-03 15:30 Kevin Lynx 阅读(2908) | 评论 (3)编辑 收藏

为什么处理排序的数组要比非排序的快?

参考Why is processing a sorted array faster than an unsorted array?

问题

看以下代码:

#include <algorithm>
#include <ctime>
#include <iostream>

int main()
{
    // generate data
    const unsigned arraySize = 32768;
    int data[arraySize];

    for (unsigned c = 0; c < arraySize; ++c)
        data[c] = std::rand() % 256;


    // !!! with this, the next loop runs faster
    std::sort(data, data + arraySize);


    // test
    clock_t start = clock();
    long long sum = 0;

    for (unsigned i = 0; i < 100000; ++i)
    {
        // primary loop
        for (unsigned c = 0; c < arraySize; ++c)
        {
            if (data[c] >= 128)
                sum += data[c];
        }
    }

    double elapsedTime = static_cast<double>(clock() - start) / CLOCKS_PER_SEC;

    std::cout << elapsedTime << std::endl;
    std::cout << "sum = " << sum << std::endl;
}

问题就在于,去掉std::sort那一行,以上代码将运行更长的时间。在我的机器上未去掉std::sort耗时8.99s,去掉后耗时24.78s。编译器使用的是gcc4.4.3。事实上,以上代码跟编译器没有关系,甚至跟语言没有关系。那这是为什么呢?

这跟处理这个数组的逻辑有非常大的关系。如以上代码所示,这个循环里有个条件判断。条件判断被编译成二进制代码后,就是一个跳转指令,类似:

具体为什么会不同,这涉及到计算机CPU执行指令时的行为。

CPU的流水线指令执行

想象现在有一堆指令等待CPU去执行,那么CPU是如何执行的呢?具体的细节可以找一本计算机组成原理的书来看。CPU执行一堆指令时,并不是单纯地一条一条取出来执行,而是按照一种流水线的方式,在CPU真正执行一条指令前,这条指令就像工厂里流水线生产的产品一样,已经被经过一些处理。简单来说,一条指令可能经过这些过程:取指(Fetch)、解码(Decode)、执行(Execute)、放回(Write-back)。

假设现在有指令序列ABCDEFG。当CPU正在执行(execute)指令A时,CPU的其他处理单元(CPU是由若干部件构成的)其实已经预先处理到了指令A后面的指令,例如B可能已经被解码,C已经被取指。这就是流水线执行,这可以保证CPU高效地执行指令。

Branch Prediction

如上所说,CPU在执行一堆顺序执行的指令时,因为对于执行指令的部件来说,其基本不需要等待,因为诸如取指、解码这些过程早就被做了。但是,当CPU面临非顺序执行的指令序列时,例如之前提到的跳转指令,情况会怎样呢?

取指、解码这些CPU单元并不知道程序流程会跳转,只有当CPU执行到跳转指令本身时,才知道该不该跳转。所以,取指解码这些单元就会继续取跳转指令之后的指令。当CPU执行到跳转指令时,如果真的发生了跳转,那么之前的预处理(取指、解码)就白做了。这个时候,CPU得从跳转目标处临时取指、解码,然后才开始执行,这意味着:CPU停了若干个时钟周期!

这其实是个问题,如果CPU的设计放任这个问题,那么其速度就很难提升起来。为此,人们发明了一种技术,称为branch prediction,也就是分支预测。分支预测的作用,就是预测某个跳转指令是否会跳转。而CPU就根据自己的预测到目标地址取指令。这样,即可从一定程度提高运行速度。当然,分支预测在实现上有很多方法。

简单的预测可以直接使用之前的实际执行结果。例如某个跳转指令某一次产生了跳转,那么下一次执行该指令时,CPU就直接从跳转目标地址处取指,而不是该跳转指令的下一条指令。

答案

了解了以上信息后,文章开头提出的问题就可以解释了。这个代码中有一个循环,这个循环里有一个条件判断。每一次CPU执行这个条件判断时,CPU都可能跳转到循环开始处的指令,即不执行if后的指令。使用分支预测技术,当处理已经排序的数组时,在若干次data[c]>=128都不成立时(或第一次不成立时,取决于分支预测的实现),CPU预测这个分支是始终会跳转到循环开始的指令时,这个时候CPU将保持有效的执行,不需要重新等待到新的地址取指;同样,当data[c]>=128条件成立若干次后,CPU也可以预测这个分支是不必跳转的,那么这个时候CPU也可以保持高效执行。

相反,如果是无序的数组,CPU的分支预测在很大程度上都无法预测成功,基本就是50%的预测成功概率,这将消耗大量的时间,因为CPU很多时间都会等待取指单元重新取指。

本文完。最后感叹下stackoverflow上这个帖子里那个老外回答问题的专业性,我要是楼主早就感动得涕泪横飞了。感谢每一个传播知识的人。

参考资料

  1. http://blog.sina.com.cn/s/blog_6c673e570100zfmo.html
  2. http://www.cnblogs.com/dongliqian/archive/2012/04/05/2433847.html
  3. http://en.wikipedia.org/wiki/Branch_predictor

posted @ 2012-08-30 17:43 Kevin Lynx 阅读(3037) | 评论 (3)编辑 收藏

MMO聊天服务器设计

MMO中的聊天服务主要功能就是做客户端之间的聊天内容转发。但是聊天的形式有很多,例如私聊、同场景聊、队伍内聊、工会内聊、全服务器聊、甚至临 时组建房间聊。这些逻辑功能其实都是可以做在逻辑服务器上的,最多改改世界服务器,但是这样完成功能的话,不免将聊天本身的逻辑与游戏逻辑关联起来。我们 希望做得更上一层,将聊天服务本身脱离开来。但是独立聊天服务还不够,因为就算独立出来了,也有可能在实现上与具体的游戏逻辑相关联。所以,我们做了进一 步的抽象,想实现一个更为通用的聊天服务器。

设计实现

实体设计

聊天这个过程,我们将其抽象为实体(entity)与实体间的对话。这个实体概念其实很宽泛。任何可接收聊天消息的都算做实体,例如单个玩家、一个 场景、一个队伍、一个房间、一个工会、甚至整个服务器。这个思想其实就是支持整个聊天服务器设计的最根本思想。最开始,我将聊天服务器分为个体和组两个概 念,其实这个抽象程度都太低,并且会导致实现上的复杂。相反,将整个系统完全使用实体这个概念来组装,就简单很多。当然,实体是有很多种类的,在处理接收 聊天消息这个动作时,其处理方式就不同。例如单个玩家实体仅做消息的发送,场景实体则是将消息发给场景内的所有玩家,队伍实体就是将消息发给队伍内的所有 玩家。从这一点来看,我们的实体种类其实并不多,因为场景、队伍这些,都是组实体(group entity)。用C++来描述:

class Entity {
public:
    
// send text to this entity
    virtual bool Send(Entity *sender, const std::string &text) = 0;

protected:
    GUID m_id;
    
int m_type;
};

class SockEntity : pubilc Entity {
public:
    
virtual bool Send(Entity *sender, const std::string &text) {
        
// find the map socket and send text to the socket
        long socket = FindSocket(this);
        Message msg(MSG_CS2E_SENDTEXT);
        msg.Add(sender
->ID());
        msg.Add(text);
        msg.SendToSocket(socket);
        
return true;
    }
};

class GroupEntity : public Entity {
public:
    
virtual bool Send(Entity *sender, const std::string &text) {
        
for (std::list<Entity*>::const_iterator it = m_mems.begin(); it != m_mems.end(); ++it) {
            (
*it)->Send(sender, text);
        }
        
return true;
    }
private:
    std::list
<Entity*> m_mems;
};


SockEntity用于表示物理上聊天服务器的客户端,例如游戏客户端。

网络拓扑

实际上,除了转发聊天内容外(Entity::Send),实体还有很多其他行为,例如最起码的,创建组实体,往组实体里添加成员等。在设计上,组 实体的创建由逻辑服务器或者其他服务器来完成,目前游戏客户端是没有创建组实体的权限的(实现上我们还为实体添加了权限验证机制)。在网络拓扑上,聊天服 务器始终是作为服务器角色,而它的客户端则包括游戏客户端、逻辑服务器、甚至其他服务器,这样聊天服务器在提供了固定的协议后,它就是完全独立的,不依赖 任何其他组件:

             CS
          
/  |  \
         
/   |   \
        
/    |    \
       GC   GC   GS

(CS: Chat Server, GC: Game Client, GS: Game Server)

基于此,我们扩充了Entity的类体系:

class ClientEntity : public SockEntity {

private:
    GUID m_gsEntity; 
// 标示该客户端实体位于哪个逻辑服务器实体上
};

class GSEntity : public SockEntity {
};

消息协议

聊天服务器的核心实现,其实就是针对以上实体做操作。因此,聊天服务器的消息协议方面,也主要是针对这些实体的操作,包括:

  • 创建

    实体的创建很简单,不同的实体其创建所需的参数都不一样。例如客户端实体创建时需要传入一个逻辑服务器实体的ID,组实体的创建可以携带组成员实体列表。 为了处理权限和安全问题,在具体实现上,逻辑服务器实体的创建是由聊天服务器本地的配置决定,即聊天服务器启动则根据配置创建好逻辑服务器实体;客户端实 体是当角色进入逻辑服务器后,由服务器创建,客户端无法创建实体。

  • 删除

    实体的删除为了处理方便,约定删除请求必须由实体的创建者发起。因为从逻辑上将,某个模块如果可以创建一个实体,那么其必然知道什么时候该删除这个实体。

  • 修改

    修改指的是修改实体内部实现的一些属性,例如组实体修改其组成员。这个操作是非常重要的。对于SockEntity而 言,修改意味着修改其连接状态,例如当逻辑服务器在聊天服务器上创建了客户端实体后,实际上此时客户端并没有在网络方面连接聊天服务器,此时这个Entity实 际上是不可用的,因为它无法用于发送消息。这个时候我们标志该实体的状态为非连接状态。当客户端主动连接上聊天服务器后,客户端就主动发起修改自己对应的 客户端实体请求,该请求将自己的状态修改为连接状态。当客户端关闭时,聊天服务器网络层接收到连接断开通知,该通知肯定是早于逻辑服务器发来的删除实体通 知的,此时将该客户端实体状态修改为断开状态,并在接收到逻辑服务器删除实体通知时将其真正删除。这里展示的这种状态修改策略,实际上在整个系统中是非常 重要的。它用于指导网络连接和上层逻辑之间的关系,因为整个聊天系统中,各个进程的状态是不可预料的(随时可能宕掉),当某个进程尤其是逻辑服务器宕掉 后,聊天服务器是得不到任何正常逻辑通知的,它只能得到网络连接的通知。

总结

整个系统实现下来,实际上是非常简单的,代码量也很少。当然还有很多细节问题,例如聊天信息中携带物品信息,这涉及到异步预处理聊天内容,这里就不 方便细说了。

原文地址: http://codemacro.com/2012/08/29/mmo-chat-server/
written by Kevin Lynx  posted at http://codemacro.com

posted @ 2012-08-29 11:37 Kevin Lynx 阅读(3513) | 评论 (2)编辑 收藏

使用memcmp比较两个变量结果一定吗?

参考Is using memcmp on array of int strictly conforming?

以下代码一定会输出ok吗?

#include <stdio.h>
#include <string.h>

struct S { int array[2]; };

int main () {
    struct S a = { { 1, 2 } };
    struct S b;
    b = a;
    if (memcmp(b.array, a.array, sizeof(b.array)) == 0) {
        puts("ok");
    }
    return 0;
}

我在vs2005以及gcc4.4.3上做了测试,都输出了ok。但这并不意味这个代码会永远输出ok。问题主要集中于这里使用了赋值语句来复制值,但却使用了memcmp这个基于内存数据比较的函数来比较值。

c语言中的赋值运算符(=)被定义为基于值的复制,而不是基于内存内容的复制。

C99 section 6.5.16.1 p2: In simple assignment (=), the value of the right operand is converted to the type of the assignment expression and replaces the value stored in the object designated by the left operand.

这个其实很好理解,尤其在不同类型的数字类型间复制时,例如:

float a = 1.1;
int b = a;

因为浮点数和整形数的内存布局不一样,所以肯定是基于值的一种复制。另外,按照语言标准的思路来看,内存布局这种东西一般都属于实现相关的,所以语言标准是不会依赖实现去定义语言的。

上面的定理同样用于复杂数据类型,例如结构体。我们都知道结构体每个成员之间可能会有字节补齐,而使用赋值运算符来复制时,会不会复制这些补齐字节的内容,是语言标准未规定的。这意味着使用memcmp比较两个通过赋值运算符复制的两个结构体时,其结果是未定的。

但是上面的代码例子中,比较的其实是两个int数组。这也无法确认结果吗?这个问题最终集中于,难道int也会有不确定的补齐字节数据?

C99 6.2.6.2 integer types For signed integer types, the bits of the object representation shall be divided into three groups: value bits, padding bits, and the sign bit. […] The values of any padding bits are unspecified.

这话其实我也不太懂。一个有符号整数int,其内也有补齐二进制位(bits)?

但无论如何,这个例子都不算严谨的代码。人们的建议是使用memcpy来复制这种数据,因为memcpy和memcmp都是基于内存内容来工作的。

posted @ 2012-08-17 14:07 Kevin Lynx 阅读(3871) | 评论 (2)编辑 收藏

让wxListCtrl支持子item编辑

我使用的wxLua版本信息为wxLua 2.8.7.0 built with wxWidgets 2.8.8,也就是LuaForWindows_v5.1.4-40.exe这个安装包里自带的wxLua。我不知道其他wxWidgets版本里wxListCtrl怎样,但我使用的版本里wxListCtrl是不支持编辑里面的子item的。在我使用的report模式下,子item也就是特定某一行一列的item。

google了一下,发现悲剧地需要自己实现,主要就是自己显示一个wxTextCtrl:

--
-- file: wxListCtrlTextEdit.lua
-- author: Kevin Lynx
-- date: 08.06.2012
--
local EditList = {}

-- get the column by an abs point
function EditList:getColumn(x)
    local cols = self.listctrl:GetColumnCount()
    local cx = 0
    for i = 0, cols - 1 do
        local w = self.listctrl:GetColumnWidth(i)
        if x <= cx + w then return i end
        cx = cx + w
    end
    return -1
end

-- when a mouse down, show a text edit control 
function EditList:onLeftDown(evt)
    if self.editor:IsShown() then
        self:closeEditor()
    end
    local p = evt:GetPoint()
    local row = evt:GetIndex()
    local col = self:getColumn(p.x)
    local rect = wx.wxListCtrlEx.GetSubItemRect(self.listctrl, row, col)
    rect:SetHeight(rect:GetHeight() + 5) -- adjust
    self.editor:SetSize(rect)
    self.editor:Show()
    self.editor:SetValue(wx.wxListCtrlEx.GetItemText(self.listctrl, row, col))
    self.editor:SetFocus()
    self.col = col
    self.row = row
end

function EditList:closeEditor()
    if not self.editor:IsShown() then return end
    self.editor:Hide()
    self.listctrl:SetItem(self.row, self.col, self.editor:GetValue())
end

function EditList:initialize()
    self.editor = wx.wxTextCtrl(self.listctrl, wx.wxID_ANY, "", wx.wxDefaultPosition, wx.wxDefaultSize, wx.wxTE_PROCESS_ENTER + wx.wxTE_RICH2)
    self.editor:Connect(wx.wxEVT_COMMAND_TEXT_ENTER, function () self:closeEditor() end)
    -- not work actually
    self.editor:Connect(wx.wxEVT_COMMAND_KILL_FOCUS, function () self:closeEditor() end)
    self.editor:Hide()
end

function wx.wxListCtrlTextEdit(listctrl)
    local o = {
        listctrl = listctrl,
        editor = nil,
    }
    local editlist = newObject(o, EditList)
    editlist:initialize()
    listctrl:Connect(wx.wxEVT_COMMAND_LIST_ITEM_RIGHT_CLICK, function (evt) editlist:onLeftDown(evt) end)
    listctrl:Connect(wx.wxEVT_COMMAND_LIST_ITEM_FOCUSED, function () editlist:closeEditor() end)
    return listctrl
end

其原理就是获取到当前鼠标点击所在的子item位置,然后在此位置显示一个wxEditCtrl即可。以上代码需要依赖我之前写的Lua里实现简单的类-对象中的代码,同时依赖以下针对wxListCtrl的扩展接口:

--
-- file: wxListCtrlExtend.lua
-- author: Kevin Lynx
-- date: 08.07.2012
-- brief: extend some util functions to wx.wxListCtrl
-- 
wx.wxListCtrlEx = {}

function wx.wxListCtrlEx.GetSubItemRect(listctrl, item, col)
    local rect = wx.wxRect()
    listctrl:GetItemRect(item, rect)
    local x = 0
    local w = 0
    for i = 0, col do
        w = listctrl:GetColumnWidth(i)
        x = x + w
    end
    return wx.wxRect(x - w, rect:GetY(), w, rect:GetHeight())
end

function wx.wxListCtrlEx.GetItemText(listctrl, item, col)
    local info = wx.wxListItem()
    info:SetId(item)
    info:SetColumn(col)
    info:SetMask(wx.wxLIST_MASK_TEXT)
    listctrl:GetItem(info)
    return info:GetText()
end

在我看到的wxWidgets官方文档里,其实wxListCtrl已经有GetSubItemRect接口,并且在另一些示例代码里,也看到了GetItemText接口,但是,我使用的版本里没有,所以只好自己写。基于以上,要使用这个可以支持编辑子item的wxListCtrl,可以:

list = wx.wxListCtrlTextEdit(wx.wxListCtrl(dialog, wx.wxID_ANY, wx.wxDefaultPosition, wx.wxDefaultSize, wx.wxLC_REPORT))

也就是通过wx.wxListCtrlTextEdit这个函数做下处理,这个函数返回的是本身的wxListCtrl。当然更好的方式是使用继承之类的方式,开发一种新的控件,但在Lua中,针对usedata类型的扩展貌似只能这样了。

最好吐槽下,这个控件扩展其实很恶心。本来我打算当编辑控件失去焦点后就隐藏它,但是往编辑控件上注册KILL_FOCUS事件始终不起作用;我又打算弄个ESC键盘事件去手动取消,但显然wxTextCtrl是不支持键盘事件的。好吧,凑合用了。

posted @ 2012-08-07 17:09 Kevin Lynx 阅读(2872) | 评论 (0)编辑 收藏

像写函数式语言代码一样写C++


忘记最早接触函数式编程语言是什么时候了,也忘记接触的第一门函数式语言是哪一门。断断续续接触过好几种函数式语言(当然都算不纯的,ruby/lisp不算纯吧),这些语言的思想在潜移默化中多多少少对我有所影响。

我是个C++程序员,我不知道我平时写的都是些什么代码。最让人印象深刻就是我会经常写遍历STL容器的代码,是经常,这样的遍历你可能也不陌生:

for (ListType::iterator it = con.begin(); it != con.end(); ++it) {
    something
}

或者针对std::map/set等的查找:

Table::iterator it = table.find(key);
if (it == table.end())
    
do-something
do-something

多亏STL接口的一致性,这让我们写出了很多“一致性“代码。慢慢地我觉得恶心,不禁想起函数式编程语言中,对于这种需求一般都会提供类似的接口:

con.map(function (it) if (it->some-filed == some-value) return something end)
# 或者
con.each 
do |it| if it.some-filed == some-value then return something end end
# 或者
(con.map (lambda (it) (
if ((= it.some-filed some-value)) (return something))))

(好吧,lisp我又忘了)总之,这种针对容器的遍历操作,都会成为一种内置接口,并且通过lambda来让用户直接编写处理代码,少去写循环的冗余。然后,我写了类似下面的一组宏(随手敲的不保证能运行):

#define IT_N __it

#define TRAVERSE_MAP(type, map, exps) \
    
for (type::iterator IT_N = map.begin(); IT_N != map.end(); ++IT_N) { \
        exps; \
    }
#define I_KEY (IT_N->first)
#define I_VALUE (IT_N->second)

#define TRAVERSE_LIST(type, list, exps) \
    
for (type::iterator IT_N = list.begin(); IT_N != list.end(); ++IT_N) { \
        exps; \
    }
#define L_VALUE (*IT_N)

#define FIND_MAP_ITEM(type, map, key, fexps, texps) \
    
do { \
        type::iterator IT_N 
= map.find(key); \
        
if (IT_N == map.end()) { \
            fexps; \
        } 
else { \
            texps; \
        } \
    } 
while(0)

#define VAL_N __val
#define FIND_LIST_ITEM_IF(type, list, cmp, fexps, texps) \
    
do { \
        
struct Comp { \
            
bool operator() (const type::value_type &VAL_N) const { \
                
return cmp; \
            } \
        }; \
        type::iterator IT_N 
= std::find_if(list.begin(), list.end(), Comp()); \
        
if (IT_N != list.end()) { \
            texps; \
        } 
else { \
            fexps; \
        } \
    } 
while(0)

#define NULL_EXP ;

当然,以上接口都还包含一些const版本,用于const容器的使用。使用的时候(截取的项目中的使用例子):

TRAVERSE_MAP(TimerTable, m_timers, 
        I_VALUE.obj
->OnTimerCancel(I_KEY, I_VALUE.arg);
        TIMER_CANCEL(I_VALUE.id)); 

TRAVERSE_LIST(AreaList, areas,
        ids.push_back(L_VALUE
->ID()));

FIND_MAP_ITEM(PropertyTable, m_properties, name,
        LogWarn(
"set a non-existed property %s", name.c_str()); return NIL_VALUE,
        
if (val.Type() != I_VALUE.type()) {
            
return NIL_VALUE; 
        } 
else {
            GValue old 
= I_VALUE;
            I_VALUE 
= val; 
            
return old;
        });

多亏了C/C++宏对一切内容的可容纳性,可以让我往宏参数里塞进像if这种复合语句,甚至多条语句(例如最后一个例子)。这些宏我使用了一段时间,开始觉得挺爽,很多函数的实现里,我再也不用写那些重复的代码了。但是后来我发觉这些代码越来越恶心了。最大的弊端在于不可调试,我只能将断点下到更深的代码层;然后就是看起来特不直观,连作者自己都看得觉得不直观了,可想而知那些连函数式编程语言都不知道是什么的C++程序员看到这些代码会是什么心情(可以想象哥已经被诅咒了多少次)。

函数式语言让人写出更短的代码,这一点也对我有影响,例如我最近又写下了一些邪恶代码:

// split a string into several sub strings by a split character i.e:
// "a;b;c;" => "a", "b", "c"
// "a;b;c" => "a", "b", "c"
std::vector<std::string> SplitString(const std::string &str, char split) {
    std::vector
<std::string> ret;
    size_t last 
= 0;
    
for (size_t pos = str.find(split); pos != std::string::npos; last = pos + 1, pos = str.find(split, last)) {
        ret.push_back(str.substr(last, pos 
- last));
    }
    
return last < str.length() ? ret.push_back(str.substr(last)) : 0, ret;
}

恶心的就是最后那条return语句,因为我需要处理”a;b;c”这种c后面没加分隔符的情况,但我并不愿意为了这个需求再写一个会占超过一行的if语句。因为,我太喜欢ruby里的if了:


do-something if exp

也就是ruby里允许这种只有一行if的代码将if放在其后并作为一条语句。我的不愿意其实是有理由的,在c/c++中有太多只有一行条件体的if语句,对这些语句参合进编程风格/可读性进来后,就不得不让你写出不安的代码,例如:

if (something) return something; // 某些编程风格里不允许这样做,因为它不方便调试

if (something) 
    
return something; // 某些风格里又有大括号的统一要求

if (something) {
    
return something; // 就算符合风格了,但这一条语句就得多个大括号
}

if (something) 
{
    
return something; // 某些风格里这大括号就更奢侈了
}

这个return除了乍看上去有点纠结外,其实也不算什么大问题,但是那个问号表达式返回的0实在没有任何意义,而正是没有意义才会让它误导人。本来我是可以写成:

return last < str.length() && ret.push_back(str.substr(last)), ret;

这样利用条件表达式的短路运算,代码也清晰多了。但是,std::vector::push_back是一个没有返回值的函数,所以。

全文完。

posted @ 2012-07-31 09:43 Kevin Lynx 阅读(3025) | 评论 (3)编辑 收藏

仅列出标题
共12页: 1 2 3 4 5 6 7 8 9 Last