之前在开发中就发现“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. CSU 1515 Sequence

    莫队算法+map #include<cstdio> #include<cstring> #include<cmath> #include<map> #i ...

  2. URL与String转换

    NSString *str = @"www.baidu.com"; NSURL *URL = [NSURL URLWithString:str];  //string>url ...

  3. MapReduce 多表连接

    题目描述: 现在有两个文件,1为存放公司名字和城市ID,2为存放城市ID和城市名 表一: factoryname,addressed Beijing Red Star,1 Shenzhen Thund ...

  4. 基于STM32的uCOS-II移植详解

    百度:基于STM32的uCOS-II移植详解 源:基于STM32的uCOS-II移植详解

  5. iOS开发 调用系统相机和相册

    调用系统相机和相册 (iPad,iPhone)打开相机:(iPad,iPhone)//先设定sourceType为相机,然后判断相机是否可用(ipod)没相机,不可用将sourceType设定为相片库 ...

  6. Android项目文件结构

    一.Project项目结构 app/build/ app模块build编译输出的目录 app/build.gradle app模块的gradle编译文件 app/app.iml app模块的配置文件 ...

  7. iOS 之 网络 NSURLSession

    NSURLSession相比NSURLConnection功能更强大,是更上层的网络封装.相比而言,普通应用场景下NSURLSession没有什么优势,但是,在程序切换到后台的情况下,NSURLSes ...

  8. 三 APPIUM Android自动化 测试初体验

    1.创建一个maven项目 成功新建工程: 编辑pom.xml,在<dependencies></dependencies>下添加appium相关依赖: <depende ...

  9. Voilin 与 乐谱

    小提琴属于高音乐器,所以它使用的是高音谱号: 用音的时候,线不够用,那就得上加线,或下加线. 小提琴的弦对应的五线谱的位置为: 第四弦,对应五线谱的下加两条线的下面 第三弦,对应五线谱的第一线的下面 ...

  10. Angular - -ngKeydown/ngKeypress/ngKeyup 键盘事件和鼠标事件

    ngKeydown/ngKeypress/ngKeyup 该指令在按键按下/按键按下/按键松开时执行指定表达式. ngKeydown和ngKeypress略有不同,目前的测试是ngKeypress针对 ...