C++,使用一个字节来存储两个变量

4

我正在处理象棋棋盘的表示,计划将其存储在32字节的数组中,每个字节用于存储两个棋子(这样每个棋子只需要4位)。

使用这种方式会导致访问特定索引的棋盘的开销增加。您认为是否可以优化此代码或完全使用不同的索引访问方法?

c++

char getPosition(unsigned char* c, int index){
    //moving pointer
    c+=(index>>1);

    //odd number
    if (index & 1){
        //taking right part
        return *c & 0xF;
    }else
    {
        //taking left part
        return *c>>4;
    }
}


void setValue(unsigned char* board, char value, int index){
    //moving pointer
    board+=(index>>1);

    //odd number
    if (index & 1){
        //replace right part
                 //save left       value only 4 bits
        *board = (*board & 0xF0) + value;
    }else
    {
        //replacing left part
        *board  = (*board & 0xF) + (value<<4);
    }
}


int main() {

    char* c = (char*)malloc(32);

    for (int i = 0; i < 64 ; i++){
        setValue((unsigned char*)c, i % 8,i);
    }

    for (int i = 0; i < 64 ; i++){
        cout<<(int)getPosition((unsigned char*)c, i)<<" ";

        if (((i+1) % 8 == 0) && (i > 0)){
            cout<<endl;
        }


    }


    return 0;
}

我同样对您在国际象棋表示法方面的意见以及上述方法的优化作为一个独立问题非常感兴趣。

非常感谢。

编辑

感谢您的回复。一段时间前,我创建了跳棋游戏,使用了64字节的棋盘表示法。这次我正在尝试一些不同的方法,只是想看看我喜欢什么。内存并不是很大的问题。位棋盘绝对在我的尝试列表中。谢谢。


3
你必须高效地实现这个板子吗?如果你没有打算使用人工智能,我认为这并不重要,而且如果你确实要使用人工智能,那么人工智能算法比在棋盘表示上节省一些字节要重要得多。总之,对我来说这看起来很好,只是把它变得更加有效似乎没什么用处。 - IVlad
2
@IVlad:他可能正在进行一种暴力 AI,通过将每个可能的移动都构建到未来并搜索理想结果来展望局面。他将需要大量空间。 - Zan Lynx
1
问题是,如果你正在进行严格的树搜索,你不需要很多棋盘的副本,对吧?你只需要当前的副本,对吧?当你探索一个移动时,你改变了棋盘,在回来时,你撤销了它。与评估棋局位置的成本相比,这将不会花费任何代价。管理数千个棋盘位置的存储成本将是丑陋的。 - Mike Dunlavey
实际上,一个字节用于表示两个“方格”,无论它们包含两个、一个或零个棋子。 - isekaijin
6个回答

9
那就是过早优化的问题了。原本需要64字节存储棋盘,现在只需要32字节,这真的对你有什么实际好处吗?你是否分析了情况以确定你需要节省这些内存?
假设你使用的是最不优秀的搜索方法之一,即没有任何启发式的直接AB搜索到深度D,并在搜索之前生成所有可能的移动,那么你的棋盘所需的绝对最大内存将是sizeof(board) * W * D。如果我们假设W = 100,D = 30,则你将在深度D上有3000个棋盘在内存中。64k和32k相比,真的值得吗?
另一方面,你增加了访问board[location]所需的操作次数,每个搜索可能会调用数百万次。
构建象棋人工智能时,你最终会寻找的主要是CPU周期,而不是内存。这可能会因你的目标是手机等设备而略有不同,但即使如此,你也会更关心速度,而不是达到足够的深度以引起任何内存问题。
至于我更喜欢哪种表示方式……我喜欢位棋盘。虽然没有进行过大量严肃的测量,但我确实比较了两个引擎,一个使用位棋盘,一个使用数组,位棋盘的速度更快,可以达到比另一个更深的搜索深度。

顺便说一句,如果你对位棋实现感兴趣,我建议你看看gnuchess、crafty和我的junfa(http://sourceforge.net/projects/xiangqi-engine/files/)。请注意,JunFa是我在大学时编写的,当时我对C++的了解不如现在。虽然它的代码相当不错,但它还没有达到我现在的标准。 - Edward Strange
深度为30只需要3000个棋盘吗?我认为你的计算有点偏差...(应该是(X^d-1)/(X-1),其中X=sizeof(board)*W - 这将为您提供大约1x10^58个棋盘,用于30层的深度即15步...) - BlueRaja - Danny Pflughoeft
@raja - 你一次只访问一个分支,如果在评估完毕后仍保留所有副本,那么这将是一个非常糟糕的实现。你会访问那么多位置,但不会在内存中拥有那么多棋盘的副本。我的数学是正确的。 - Edward Strange
当我还是个在伊利诺伊州北部农场长大的孩子时,我们常说“我妈妈做的面包几乎和超市买的一样好”,但我们只是开玩笑。我猜如果足够多的人这么说,它就会成为官方用语。我敢打赌下一个可能是nu-cu-lar :-) - Mike Dunlavey

3

我要先指出一个可能存在的bug(取决于编译器和编译器设置)。而且由于bug是早期优化的罪魁祸首,我们应该避免进行过度的优化。

   //taking left part
    return *c>>4;

如果*c是负数,则>>可能会重复负高位。即在二进制中:
0b10100000 >> 4 == 0b11111010

对于一些编译器(例如C++标准将其留给编译器决定 - 包括是否携带高位和字符是有符号还是无符号)。

如果您确实想要继续使用打包的位(让我说一句,您可能不应该费心,但这取决于您),我建议将打包的位封装到一个类中,并覆盖[]使其如下:

board[x][y] 

这段代码提供给你未打包的数据位。然后,你可以轻松地开启和关闭打包,并且在任何情况下都具有相同的语法。如果你内联操作符重载,它应该与你现在拥有的代码一样高效。


好的提醒。你应该指出这仅适用于有符号类型。除非我弄错了,否则对无符号值进行右移操作不应该进行符号扩展。还要注意,char 的有符号性取决于实现(例如,GCC 在 PowerPC/Linux 上默认为无符号 char)。 - Joey Adams
是的,有符号部分稍微被忽略了,我只是在写“一个char是有符号还是无符号”时给出了一些暗示。我可以更明确地表达。 - tony

2

64字节的RAM非常少。你最好使用char[8][8]。除非你打算存储大量的棋盘。使用char[8][8]可以更容易(和更快)地访问棋盘并对其进行更复杂的操作。

如果您仍然有兴趣以紧凑表示形式存储棋盘(无论是为了练习还是存储大量棋盘),我认为您在位运算方面做得很好。如果您想追求速度,可以考虑使用inline关键字内联访问器。


3
如果你打算制作任何类型的国际象棋人工智能,你需要存储如此多的棋盘,以至于空间会成为一个问题。 - JoeG
+1 我在一枚只有176个字节的RAM的PIC16c66上写了一个国际象棋游戏,它可以预测5步棋。棋盘存储在64个字节中。在搜索树期间,移动是在棋盘上进行的,并且移动被“撤消”以返回树的上层。内存中只有1个棋盘实例。如果要在哈希表中存储访问的位置,则另当别论。 - phkahler
@joe - 我非常怀疑。正如我在我的回答中所展示的,内存问题甚至不是一个很大的考虑因素,即使做出一些极端的假设。实际上,大多数国际象棋引擎在搜索过程中最多只会有深度副本,并且除非你让它们在一个移动上运行数小时(进行分析),否则大多数引擎甚至从未达到深度> 20。 - Edward Strange
Joe - 这是为了只节省2倍的空间而产生了大量额外的循环。很少需要这么多的工作(或让计算机每次访问板子时都要经过这么多的工作)才能以2倍的优势获胜。 - miked

0

空间是否足够考虑,以至于您不能只使用一个完整的字节来表示一个正方形?这将使程序中的访问更容易跟踪,并且由于不需要位操作,速度可能更快。

否则,为了确保一切顺利,我会确保所有类型都是无符号的:getPosition返回无符号字符,并使用“U”限定所有数字文字(例如0xF0U),以确保它们始终被解释为无符号。最有可能您不会遇到任何有符号问题,但为什么要冒险在某些行为出乎意料的架构上呢?


0

代码写得不错,但如果你真的想深入优化性能,最好了解一下你特定的CPU架构。

据我所知,将棋子存储在8个字节中可能更有效率。即使你递归15步,L2缓存大小也很难成为限制因素,但RAM未对齐可能会成为限制因素。我猜一个正确处理棋盘的算法应该包括Expand()和Reduce()函数,在算法的不同部分之间转换棋盘表示:有些可能在紧凑表示上更快,有些则相反。例如,缓存和涉及通过两个相邻单元格的组合进行哈希的算法可能适用于紧凑结构,其他情况则不然。

如果性能如此重要,我还建议考虑开发一些辅助硬件,比如FPGA板或GPU代码。


0
作为一个国际象棋玩家,我可以告诉你:一个棋局的胜负不仅仅取决于每个棋子的位置。你还需要考虑以下几点:
  1. 哪一方是下一步行动的玩家?
  2. 白方和/或黑方是否可以进行王翼或后翼易位?
  3. 兵是否可以进行“吃过路兵”?
  4. 自上次兵或棋子被吃掉以来经过了多少步?
如果你用于表示棋局的数据结构没有反映这些信息,那么你就会遇到大麻烦。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接