First we try, then we trust

  博客园 :: 首页 :: 新随笔 :: 联系 :: 订阅 订阅 :: 管理 ::
  183 随笔 :: 111 文章 :: 2953 评论 :: 297 Trackbacks
最近在给华容道程序进行性能调优时遇到了一个令人费解的效率问题,我最初始的代码是这样写的:

    public override void CheckAvailableSteps(BlankPosition _blankPosition, CallBackDelegate _callback)
    
{
      
if(CanMoveUp(_blankPosition))
        _callback(_newPosition, _newBlankPosition, MoveMethod.Up);

      
if(CanMoveDown(_blankPosition))
        _callback(_newPosition, _newBlankPosition, MoveMethod.Down);

      
if(CanMoveLeft(_blankPosition))
        _callback(_newPosition, _newBlankPosition, MoveMethod.Left);

      
if(CanMoveRight(_blankPosition))
        _callback(_newPosition, _newBlankPosition, MoveMethod.Right);

      
if(CanMoveLeft2(_blankPosition))
        _callback(_newPosition, _newBlankPosition, MoveMethod.Left2);

      
if(CanMoveRight2(_blankPosition))
        _callback(_newPosition, _newBlankPosition, MoveMethod.Right2);

      
return;
    }

  }

这是一个水平横放棋子的走法判断程序。可以看出来,我依次测试了一个水平棋子的六种走法是否可行。后来我认为可以进一步进行调优:比如说如果该棋子可以上移,那么其它五种移法就不用考虑了,肯定不行。类似如果该棋子可以左移两位,那么一定可以左移一位。这样我就可以减少判断的次数,进而提高程序运行的效率。所以我把程序改成了类似这个样子:

    public override void CheckAvailableSteps(BlankPosition _blankPosition, CallBackDelegate _callback)
    
{
      
if(CanMoveUp(_blankPosition))
      
{
        _callback(_newPosition, _newBlankPosition, MoveMethod.Up);
        
return;
      }


      
if(CanMoveDown(_blankPosition))
      
{
        _callback(_newPosition, _newBlankPosition, MoveMethod.Down);
        
return;
      }


      
if(CanMoveLeft2(_blankPosition))
      
{
        _callback(_newPosition, _newBlankPosition, MoveMethod.Left2);
        
        
// 此处添加MoveLeft代码并CallBack, 略
        
        
return;
      }


      
if(CanMoveRight2(_blankPosition))
      
{
        _callback(_newPosition, _newBlankPosition, MoveMethod.Right2);
        
        
// 此处添加MoveRight代码并CallBack, 略
        
        
return;
      }


      
if(CanMoveLeft(_blankPosition))
        _callback(_newPosition, _newBlankPosition, MoveMethod.Left);

      
if(CanMoveRight(_blankPosition))
        _callback(_newPosition, _newBlankPosition, MoveMethod.Right);

      
return;
    }

  }

结果猜怎么着?程序运行效率骤然降低,从原来的5.88秒一下子长到了6.23秒,这可不是一个小数。明明添加了一些return避免过多的运算,可系统效率反而下降,真不知道为什么?难道代码运行效率与代码行数还有关系?

放到“新手区”。我想原因可能有两个:1、此处的修改对效率影响太小以至于可以忽略不计。2、测试误差比较大。我已经放弃这种优化方法了。
posted on 2005-02-05 11:16 吕震宇 阅读(818) 评论(13)  编辑 收藏

评论

....,这要根据你测试的那一次具体程序运行的“路径”把
比如,刚好运行时的条件是moveleft2的
显然你优化后的moveleft2做的动作比优化前的多,那自然时间就长拉
  回复  引用    

#2楼  2005-02-05 13:35 KingofSC [未注册用户]
我错了,没看清楚,不好意思
  回复  引用    

#3楼  2005-02-05 14:14 Ya_Mu [未注册用户]
我想是这样的,首先CanMove...的判断并不会占多少时间,而导致时间延长的原因可能是因为改后改变了程序运行时最后一轮(即出最终答案的那一轮里答案出来的次序,也许本来是一上来就出现的,但现在变成最后才出来),如果换一个情况进行试验而时间和原来差不多的话,那这个猜测就是正确的。不知道是不是这样........
能不能看到你的最新的代码呢?
  回复  引用    

#4楼 [楼主] 2005-02-05 14:47 吕震宇      
@Ya_Mu

在没有优化的情况下,我需要将所有的六次情况都判断一下。而修改后,即使最后一轮,也和原来一样判断五次就行了。没有任何区别。可效率确孑然不同。

最新的代码只是合并了些命名空间,删除了部分接口。你可以在老程序上将Layout项目中VChessman代码中的CheckAvailableSteps方法替换成下面的代码,然后在测试时测试NormalLayoutTest的运行时间,你就会发现其中的差异。如果其它棋子也修改的化,差异就更大了。

public override void CheckAvailableSteps(BlankPosition _blankPosition, CallBackDelegate _callback)
{
if(CanMoveLeft(_blankPosition))
{
_callback(_newPosition, _newBlankPosition, MoveMethod.Left);
return;
}

if(CanMoveRight(_blankPosition))
{
_callback(_newPosition, _newBlankPosition, MoveMethod.Right);
return;
}

if(CanMoveUp2(_blankPosition))
{
_callback(_newPosition, _newBlankPosition, MoveMethod.Up2);

// 设置棋子新位置
_newPosition.x = _position.x;
_newPosition.y = _position.y - 1;

// 设置新的空白位置
_newBlankPosition.Pos1.x = _position.x;
_newBlankPosition.Pos1.y = _position.y + 1;
_newBlankPosition.Pos2.x = _position.x;
_newBlankPosition.Pos2.y = _position.y - 2;

_callback(_newPosition, _newBlankPosition, MoveMethod.Up);

return;
}

if(CanMoveDown2(_blankPosition))
{
_callback(_newPosition, _newBlankPosition, MoveMethod.Down2);

// 设置棋子新位置
_newPosition.x = _position.x;
_newPosition.y = _position.y + 1;

// 设置新的空白位置
_newBlankPosition.Pos1 = _position;
_newBlankPosition.Pos2.x = _position.x;
_newBlankPosition.Pos2.y = _position.y + 3;

_callback(_newPosition, _newBlankPosition, MoveMethod.Down);

return;
}

if(CanMoveUp(_blankPosition))
_callback(_newPosition, _newBlankPosition, MoveMethod.Up);

if(CanMoveDown(_blankPosition))
_callback(_newPosition, _newBlankPosition, MoveMethod.Down);

return;
}
  回复  引用  查看    

#5楼 [楼主] 2005-02-05 14:52 吕震宇      
上面代码的意思是说,一个竖放的棋子,如果能左移,则不会再有其它移动方法,因为只有两个空白位置,因此直接return,右移同理。

如果能上移两步,则一定能上移一步,除了这两种情况也没有别的什么情况,所以也可以return了。下移两步同理。

其它就是单独上移一步或下移一步了。从各方面将,该代码的判断次数都少于原来的判断次数,可效率确很差,不知怎么回事。
  回复  引用  查看    

#6楼  2005-02-05 15:29 Y [未注册用户]
我想你是没有理解我的意思,因为你用的是BFS,所以像金字塔一样,越到后来越慢,到最后一轮的运算差不多就是百万数量级的,所以如果正好本来是在那轮前面出来的,现在在后面出来的话,影响就差不多有1秒钟 (轮,就是你的那个“深度”)
您认为会不会是这个原因呢?
  回复  引用    

#7楼 [楼主] 2005-02-05 17:11 吕震宇      
我想应当不是这个原因。我测试了一下,原有方法搜索层级是81层,搜索布局23844个,用时3.89秒;修该后搜索层级81层,搜索布局23844个,用时3.95秒。可以看到效率反而下降了,而深度与搜索的节点都没有变化。
  回复  引用  查看    

#8楼  2005-02-14 11:03 sumtec      
老大,你的Post里面的是对lLeft/Right进行优化,可是回复里面的代码里面怎么又是对Up/Down进行优化?

而且按照这里所看到的情况,应该是
if(CanMoveDown2(_blankPosition))
{
_callback(_newPosition, _newBlankPosition, MoveMethod.Down2);

_callback(_newPosition, _newBlankPosition, MoveMethod.Down);

return;
}

不太明白为什么有下面这些代码?感觉似乎是多余的,如果不是多余的估计就是有其他的缺陷。

// 设置棋子新位置
_newPosition.x = _position.x;
_newPosition.y = _position.y + 1;

// 设置新的空白位置
_newBlankPosition.Pos1 = _position;
_newBlankPosition.Pos2.x = _position.x;
_newBlankPosition.Pos2.y = _position.y + 3;

  回复  引用  查看    

#9楼 [楼主] 2005-02-14 16:44 吕震宇      
@sumtec

我在评论中写的是一个竖放棋子。而文章中是一个横放棋子。

在我的华容道文章中写了基本算法:就是一个棋子的移动只与该棋子以及空格的位置有关,与其它棋子的位置无关。所以一旦棋子可以移动,我就算出移动后棋子的新位置以及空格的位置。所以一个竖放的棋子一旦可以向上移动两步,它一定可以向上移动一步。

在CanMoveUp2的代码中已经计算出了移动后棋子的新位置_newPosition和空格的新位置_newBlankPosition,所以直接callBack就可以了。然后的代码(下面的代码可能有所出入,但意思是这样的):

// 设置棋子新位置
_newPosition.x = _position.x;
_newPosition.y = _position.y + 1;

// 设置新的空白位置
_newBlankPosition.Pos1 = _position;
_newBlankPosition.Pos2.x = _position.x;
_newBlankPosition.Pos2.y = _position.y + 3;

这是计算向上移动一格后棋子与空格的位置。

这样一个竖放棋子如果可以向上移动两格,那么它还可以向上移动一格,除此之外就没有其它的移动方法了,就可以直接RETURN了。这就是我的本意。可效率确不高。

我已经放弃这种优化了。
  回复  引用  查看    

#10楼  2005-02-18 12:05 Sumtec      

@ 吕震宇 :

我想你可能没有明白我的意思,你先看看你在回复里面的代码:

if(CanMoveDown2(_blankPosition)) 

_callback(_newPosition, _newBlankPosition, MoveMethod.Down2); 

// 设置棋子新位置 
_newPosition.x = _position.x; 
_newPosition.y 
= _position.y + 1

// 设置新的空白位置 
_newBlankPosition.Pos1 = _position; 
_newBlankPosition.Pos2.x 
= _position.x; 
_newBlankPosition.Pos2.y 
= _position.y + 3


//// ***** 请注意这一句 *****
_callback(_newPosition, _newBlankPosition, MoveMethod.Down); 

return
}
 



既然前面的
// 设置棋子新位置 
_newPosition.x = _position.x; 
_newPosition.y 
= _position.y + 1
// 设置新的空白位置 
_newBlankPosition.Pos1 = _position; 
_newBlankPosition.Pos2.x 
= _position.x; 
_newBlankPosition.Pos2.y 
= _position.y + 3

已经是计算了向下一步的棋子位置和空白位置,那么为什么还需要调用后面的
_callback(_newPosition, _newBlankPosition, MoveMethod.Down);
 
是否他们都重复的作了同一件事情?总之这么写,我估计比较有可能问题出在这里。

另外顺便讨论一下,AVLTree是否可以用HashTable来替代,毕竟你的目的不是要排序,而是要检验是否容易产生重复。从纯粹的理论上来说,HashTable在这方面的性能消耗应该比AVLTree要好一些,因为AVLTree的复杂度至少是O(Log2(x))级别的,而HashTable则接近于O(C)级别的。同时AVLTree至少有左右节点的额外开销,而HashTable则只有一个HashCode的额外开销,它们之间的空间损耗应该不会有太大的差距,甚至有可能HashTable更加符合要求。您可以试一下,我估计如果你事先在构造的时候设置HashTable的容量为一个较大值会更好,因为在.NET的HashTable代码里面我们可以看到,容量一旦改变,HashTable内部的每一个元素的位置可能就需要重新计算,重新计算的时间取决于什么时候进行容量改变之后第一次的访问。而在这一个特定用例下面,每一次计算都必须首先访问,因此容量的改变必然会造成一定的性能损失。

  回复  引用  查看    

#11楼 [楼主] 2005-02-18 16:15 吕震宇      
@Sumtec

非常感谢提出这么多建议。我会考虑抽时间修改一下华容道程序的,出一个“性能优化版”,HashTable是一个非常好的数据结构设计,我会采纳进去,只 是HashCode的计算方法需要考虑一下。同时我考虑放弃4字节布局表示,这样排序操作也没有必要了,棋局改用64位二进制位表示(Int64)。理论 上性能应当进一步提升,内存占用有可能也会提升。

关于代码的问题,我在《华容道与数据结构 (6)》的Chessman设计中说明了callback的设计。这里是一个回调函数,根据当前布局以及棋子移动后的位置和空格位置生成一个新的布局。

CanMoveDown2函数用来判断当前棋子是否可以向下移动两格。如果可以,则计算出棋子新位置以及空格新位置(注意这个计算操作是在 CanMoveDown2函数中完成的),因此当CanMoveDown2返回逻辑真值时就直接callback。此时的newPosition与 newBlankPosition是由CanMoveDown2计算完成的值。在代码中就是:

//--------------------------------------------------------------
if(CanMoveDown2(_blankPosition))
{
  _callback(_newPosition, _newBlankPosition, MoveMethod.Down2);
//--------------------------------------------------------------

此时callback后,Layout就根据提供的数据生成了一个布局,该布局是当前棋子向下移动两格后的布局。到此还没有完,一个竖放棋子可以向下移动 两格则一定可以移动一格。所以下一步我们就没有必要再判断CanMoveDown了(CanMoveDown一定返回真),直接计算向下移动一格后棋子的 新位置与空格的新位置,然后callback,再让Layout生成另外一个全新的布局,该布局是棋子向下移动一格后的布局。

//--------------------------------------------------------------
// 设置棋子新位置(向下移动一格
_newPosition.x = _position.x;
_newPosition.y = _position.y + 1;

// 设置新的空白位置
_newBlankPosition.Pos1 = _position;
_newBlankPosition.Pos2.x = _position.x;
_newBlankPosition.Pos2.y = _position.y + 3;

_callback(_newPosition, _newBlankPosition, MoveMethod.Down);
//--------------------------------------------------------------

关键在于,后一个callback是生成另外一个布局,该布局是向下移动一格棋子后的布局。此callback非彼callback。并且棋子不再可能有 其它的移动方法了,棋子一定不能左、右、上移动,直接return了。每次调用callback,系统就生成一个全新的布局供判断。不知道我这里有没有说 清楚这个问题。

  回复  引用  查看    

#12楼  2005-02-19 00:15 sumtec      
我原来就注意到MoveMethods.Down何Down2的区别了,知道此callback非比callback,这就是我原来所说的“有其他的缺陷”的意思。

那现在我明白了,我觉得:

// 设置棋子新位置(向下移动一格)
_newPosition.x = _position.x;
_newPosition.y = _position.y + 1;

// 设置新的空白位置
_newBlankPosition.Pos1 = _position;
_newBlankPosition.Pos2.x = _position.x;
_newBlankPosition.Pos2.y = _position.y + 3;

这几句话应该放到相应的CallBack函数里面去,这不是你这个CheckAvailableSteps函数的职责,应该是MoveMethod.Down里面的职责。你现在的写法至少是职责混乱,因为Down2的callback是计算并产生布局的,而Down则不计算只产生布局。如果你让我来写一个UpLeft的CallBack,我还真不知道该怎么写,到底要不要计算位置。因为从抽象的角度来看,所有的MoveMethod的抽象含义是一样的,但事实却不是如此。

如果更严格一点说,MoveMethods这一系列的回调函数,无论从命名的字面理解上看,还是从代码逻辑分析上看,都应该包含计算和产生两个步骤——其中计算只的是根据现有布局计算出新的布局的样式,而产生指的是将新的布局放到队列里面去(包括检查是否重复等逻辑)。
  回复  引用  查看    

#13楼 [楼主] 2005-02-19 08:43 吕震宇      
@Sumtec

正如你所说,有些“职责混乱”。我也准备重构代码。不过有一点:callback只产生布局,而不进行任何计算。Down2是在CanMoveDown2函数中完成计算工作的,然后在CheckAvailableSteps函数中callback,可到了Down,就在CheckAvailableSteps函数中完成计算,然后callback,这就是“职责混乱”。

我现在还在使用:

public override void CheckAvailableSteps(BlankPosition _blankPosition, CallBackDelegate _callback)
{
if(CanMoveUp(_blankPosition))
_callback(_newPosition, _newBlankPosition, MoveMethod.Up);

if(CanMoveDown(_blankPosition))
_callback(_newPosition, _newBlankPosition, MoveMethod.Down);

if(CanMoveLeft(_blankPosition))
_callback(_newPosition, _newBlankPosition, MoveMethod.Left);

if(CanMoveRight(_blankPosition))
_callback(_newPosition, _newBlankPosition, MoveMethod.Right);

if(CanMoveLeft2(_blankPosition))
_callback(_newPosition, _newBlankPosition, MoveMethod.Left2);

if(CanMoveRight2(_blankPosition))
_callback(_newPosition, _newBlankPosition, MoveMethod.Right2);

return;
}

来进行判断,以后我会考虑重新设计这部分内容。
  回复  引用  查看    


标题  
姓名  
主页
Email (只有博主才能看到) 
验证码 *  看不清,换一张 [登录][注册]
内容(请不要发表任何与政治相关的内容)  
  登录  使用高级评论  新用户注册  返回页首  恢复上次提交      
该文被作者在 2005-02-05 18:46 编辑过