之前在开发中就发现“dirty”是一种不错的解决方案:可以用来延缓计算或者避免不必要的计算。后来在想,这应该也算一种设计模式吧,于是搜索“Dirty设计模式”,没有什么结果,然后换成英文“Dirty design pattern”,搜到了《game programming patterns》这本电子书。书中介绍了Dirty Flag 模式在游戏客户端的应用场景,如果英文不好,这里也有中文翻译。本文结合几个具体的例子,介绍什么是Dirty Flag 模式,并分析该模式的适用场景以及使用注意事项。

什么是Dirty Flag:

  简单来说,就是用一个标志位(flag)来表示一组数据的状态,这些数据要么是用来计算,或者用来需要同步。在满足条件的时候设置标志位,然后需要的时候检查(check)标志位。如果设置了标志位,那么表示这组数据处于dirty状态,这个时候需要重新计算或者同步。如果flag没有被设置,那么可以不计算(或者利用缓存的计算结果)。另外,在两次check之间,即使有多次标志位的设置,也只需要计算一次。

  因此,Dirty Flag模式的本质作用在于:延缓计算或数据同步,甚至减少无谓的计算或者同步。计算比较容易理解,对于同步,后面也会给出例子。在后面的描述中,除非特殊说明,计算也包含了同步。

Dirty Flag使用实例:

  首先,《game programming pattern》中的例子非常形象生动,图文并茂,建议直接阅读原文,本文不再复述。接下来介绍几个其他的例子。

First

    游戏开发中,有大量的物体(Entity)需要每帧tick(AI、位移),每次tick的时候检查一些条件然后做出反应。对于一些entity,可能tick检查之后发现什么都不用做,但每次tick检查也比较耗时,而且出现这种情况的概率还很高。
    利用dirty可以改造一些
 def set_need_tick(self, is_need):
self.need_tick = is_need def tick(self):
if self.need_tick:
self.do_tick() # do_tick 需要做大量的检查,较为耗时
  上面的代码每次调用tick的时候用一次条件判断,还可能嵌套一次函数调用,修改后代码如下:
 def dummy_tick(self):
pass
def set_need_tick(self, is_need):
if is_need:
self.tick = self.do_tick
else:
self.tick = self.dummy_tick
    不过上述的代码也是空间换时间,因为每个实例都增加了一个tick属性(原来是类属性)

Second

  之前在看bottle的代码时,看到了下面这个property,其作用是在首次调用的时候计算属性的值,之后就不用重新计算了。
 class cached_property(object):
""" A property that is only computed once per instance and then replaces
itself with an ordinary attribute. Deleting the attribute resets the
property. """ def __init__(self, func):
update_wrapper(self, func)
self.func = func def __get__(self, obj, cls):
if obj is None: return self
value = obj.__dict__[self.func.__name__] = self.func(obj)
return value
  如果一时不能理解上面的代码,可以参见这篇文章《python属性查找》。bottle中这个例子,前提是这个属性一旦计算了就不会再重新计算,如果应用场景在某些情况下需要重新计算呢?那么可以增加这么一个函数:
     def set_property_dirty(self, property_name):
self.__dict__.pop(property_name, None)

  在需要的时候调用这个设置函数就行了,在这个例子中,并没有对某个属性的设置和检查,但配合之前的cached_property,作用是很明显的:缓存计算结果,需要的时候重新计算。下面是完整测试代码

  

 import functools, time
class cached_property(object):
""" A property that is only computed once per instance and then replaces
itself with an ordinary attribute. Deleting the attribute resets the
property. """ def __init__(self, func):
functools.update_wrapper(self, func)
self.func = func def __get__(self, obj, cls):
if obj is None: return self
value = obj.__dict__[self.func.__name__] = self.func(obj)
return value class TestClz(object):
@cached_property
def complex_calc(self):
print 'very complex_calc'
return sum(range(100)) def __set_property_dirty(self, property_name = 'complex_calc'):
self.__dict__.pop(property_name, None) def some_action_effect_property(self):
self.__set_property_dirty() if __name__=='__main__':
t = TestClz()
print '>>> first call'
print t.complex_calc
print '>>> second call'
print t.complex_calc
print '>>>third call'
t.some_action_effect_property()
print t.complex_calc

cache property and dirty

Third

  游戏数据存档,游戏中的大量对象都需要持久化(存档),存档有各种不同的策略。第一种,每次属性变化的时候存档,这样数据不容易丢失,但是往往会存在冗余,数据库压力也较大,比如属性A的变化影响到属性B(经验的增加导致等级的变化),那么属性A变化的时候进行一次存档,属性B变换的时候又要存档。另一种策略是,定期存档,即以固定的时间间隔进行一次存档,当然可以进一步,在需要持久话的数据变化时设置dirty flag,在定期存档的时机只有设置了dirty flag才真正存储。
 

Fourth

  这个例子是Dirty Flag的升级版本,暂且称之为tag Flag吧。比如页面上有一些图表,图表是通过大量数据的数据计算然后绘制出来的,图表内容可以通过用户主动点击刷新或者定时刷新。简单的策略是每次刷新的时候服务器返回所有数据,浏览器重新显示。但事实上服务端数据变化可能不那么频繁,既浪费了大量的带宽,又让浏览器无谓的重复绘制。
  其中一种解决的办法,就是为数据增加一个签名--tag(自增整型),对于服务器端,每次数据变化的时候tag += 1。浏览器首次请求的时候获取数据以及当前数据对应的tag,之后请求的时候携带tag与服务端的tag做比较,若tag一致,则无需更新数据,否则返回新的数据和新的tag。这样,所有的客户端都能利用这个tag来决定自己是否要刷新数据。
 

Fifth

  Dirty在web前端还有许多其他应用,比如angularJSKnockoutJS,由于本人并不熟悉web前端,感兴趣的读者可以参考链接

适用场景:

    正如《game programming patterns》中的归类, Dirty Flag pattern属于optimization pattern,只有需要优化的时候才考虑使用该模式。有人说,”过早的优化是万恶之源“,我觉得这对于Dirty Flag还是比较合适的,Dirty Flag的使用不是那么自然,跟业务逻辑本身也是无关的,只有在Profile确定瓶颈之后再来考虑是否可以用Dirty Flag优化。
    某些计算(或者同步)较为昂贵且频繁,但事实上很多情况无需计算(或者同步),通过Dirty Flag来标志真正需要计算(或者同步)的情况,降低开销。
 

使用条件:

  当满足下面所有条件,或者说权衡下面的所有条件之后还可以接受,那么才建议使用Dirty Flag模式。
第一:单次计算的开销足够大
    这个是首要条件,如果每次计算的开销非常小,那么就没有必要用Dirty Flag来优化了,因为增加标志位既增加了编码复杂度,又带来了一定的开销(标志位的设置与清除)。单次计算的开销可以通过profile来确定。
 
第二:事实上需要计算的概率足够低
  我们还是以游戏为例,假设游戏的是60帧,即每秒tick60次。如果一个计算每次(每帧)都有很大的概率“必须重新计算,那dirty flag反而增加了额外的开销。
 
第三:延迟计算没有副作用
  如《game programming pattern》中的例子所示,Dirty Flag在这个例子导致延迟计算,延迟计算会将分散在不同时间进行的计算集中到检查的时刻,这样可能带来一些副作用,比如造成游戏卡顿。另外,对于游戏存档的例子,如果在两次定期存储之间服务器宕机,可能会数据丢失。
 
第四:内存换速度的代价
    使用dirty模式,很多时候需要缓存结果,这需要额外的内存,对于某些场景,还有缓存命中率的问题,在内存稀缺的移动设备上尤其需要权衡。
 

注意事项:

第一:标志位的设置必须覆盖到所有可能影响的地方
  如果某些操作遗漏了对标志位的位置,那么往往会导致严重的错误。这个也跟标志位的粒度有关。大多数情况都是因为对某个属性的修改,导致需要重新计算,在python语言中要监控到属性的修改还是很容易的,可以虫子__setattr__函数,或者使用property descriptor
 
第二:计算之后reset标志位
 
 
references:

Dirty Flag 模式及其应用的更多相关文章

  1. lazy ideas in programming

    lazy形容词,懒惰的,毫无疑问是一个贬义词.但是,对于计算机领域,lazy却是非常重要的优化思想:把任务推迟到必须的时刻,好处是避免重复计算,甚至不计算.本文的目的是抛砖引玉,总结一些编程中的laz ...

  2. python性能优化

      注意:本文除非特殊指明,”python“都是代表CPython,即C语言实现的标准python,且本文所讨论的是版本为2.7的CPython. python为什么性能差: 当我们提到一门编程语言的 ...

  3. lazy ideas in programming(编程中的惰性思想)

    lazy形容词,懒惰的,毫无疑问是一个贬义词.但是,对于计算机领域,lazy却是非常重要的优化思想:把任务推迟到必须的时刻,好处是避免重复计算,甚至不计算.本文的目的是抛砖引玉,总结一些编程中的laz ...

  4. Redis——学习之路二(初识redis服务器命令)

    上一章我们已经知道了如果启动redis服务器,现在我们来学习一下,以及如何用客户端连接服务器.接下来我们来学习一下查看操作服务器的命令. 服务器命令: 1.info——当前redis服务器信息   s ...

  5. 【redis使用全解析】常见运维操作

    作者:gnuhpc 出处:http://www.cnblogs.com/gnuhpc/ 1.1 启动 1.1.1 启动redis $ redis-server redis.conf 常见选项: ./r ...

  6. Unity ShaderLab学习总结

    http://www.jianshu.com/p/7b9498e58659 Unity ShaderLab学习总结 Why Bothers? 为什么已经有ShaderForge这种可视化Shader编 ...

  7. Inside of Jemalloc

    INSIDE OF JEMALLOCThe Algorithm and Implementation of Jemalloc author: vector03mail:   mmzsmm@163.co ...

  8. cocod2d-x 之 CCDirector、CCScene、CCSprite

    CCDirector是控制游戏流程的主要组件. typedef enum { /// sets a 2D projection (orthogonal projection)2D投机模式 kCCDir ...

  9. 查看Redis信息和状态

    原文转自:http://redisdoc.com/server/info.html INFO [section] 以一种易于解释(parse)且易于阅读的格式,返回关于 Redis 服务器的各种信息和 ...

随机推荐

  1. jQuery扩展两类函数(对象调用,静态调用)

    作者:zccst 先看小例子: $(function(){ //扩展方式1-通过对新调用 $.fn.each1=function(){ console.log("hehehehe$.fn.f ...

  2. 2016年最全面的VR资源盘点,不只有VR视频播放器还有具体到步骤的VR资源

    2016年过去了,有多少人开始使用VR来观看我们喜欢的视频资源呢?比传统视频更高的沉浸感,甚至在VR眼镜的视角中,自己仿佛化生成视频中的主角一般.然而,这种体验只有VR眼镜还是不行的,还需要有一个VR ...

  3. [bzoj1195] [hnoi2006] 最短母串

    本题是一个经典的状压dp问题,在紫书中有着加强版的例题. 本题的难度主要体现在:如何输出字符串字典序最小. 为了解决这个问题,我们有两种常用方案: 1) 我们可以采用bfs输出路径的方法,使用+1来输 ...

  4. matlab find函数

    find函数用来获取向量或矩阵中非0元素的索引 假设X是行向量,则find(X)返回的是一个行向量:X'为列向量,find(X')返回的是一个列向量. 看一个例子:A是一个行向量,B是一个列向量,将B ...

  5. Badboy安装与使用

    Badboy是一个录制web脚本的工具 1.下载Badboy:http://www.badboy.com.au/download/add 2.启动Badboy,认识主界面 3.使用Badboy录制we ...

  6. 【java基础】接口VS抽象类

    1.至少有一个被abstract修饰的方法,同时修饰类名的类为抽象类,抽象的方法必须被子类覆盖,抽象的类必须被继承,抽象的类可以包含非抽象方法,只能单继承. 2.接口中所有的变量是static fin ...

  7. Python3基础 在print中用 %d 输出一个整数

    镇场诗: 诚听如来语,顿舍世间名与利.愿做地藏徒,广演是经阎浮提. 愿尽吾所学,成就一良心博客.愿诸后来人,重现智慧清净体.-------------------------------------- ...

  8. Angular - - filter 过滤器

    Filter Ng里的过滤器. currency:把一个数字格式化成货币模式(如$1,234.56).当没有提供任何货币符号时,默认使用当前区域的符号. 使用: HTML:{{ currency_ex ...

  9. js框架Modernizr是什么东西? 他是前端开发HTML5和CSS3的强有力前端js检测类库

    最近在研究modernizr的前端框架,发现这个Modernir对前端写页面非常友好,并且能够很快的建立起适应任何设备的html页面哦.在这里分享下基础教程,让大伙对modernizr是什么?做什么用 ...

  10. Java数据库连接--JDBC调用存储过程,事务管理和高级应用

    相关链接:Jdbc调用存储过程 一.JDBC常用的API深入详解及存储过程的调用 1.存储过程的介绍 我们常用的操作数据库语言SQL语句在执行的时候要先进行编译,然后执行,而存储过程是在大型数据库系统 ...