Dirty Flag 模式及其应用

  之前在开发中就发现“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可以改造一些

1 def set_need_tick(self, is_need):
2     self.need_tick = is_need
3
4 def tick(self):
5     if self.need_tick:
6         self.do_tick() # do_tick 需要做大量的检查,较为耗时

  上面的代码每次调用tick的时候用一次条件判断,还可能嵌套一次函数调用,修改后代码如下:

1 def dummy_tick(self):
2     pass
3 def set_need_tick(self, is_need):
4     if is_need:
5         self.tick = self.do_tick
6     else:
7         self.tick = self.dummy_tick

不过上述的代码也是空间换时间,因为每个实例都增加了一个tick属性(原来是类属性)

Second

  之前在看bottle的代码时,看到了下面这个property,其作用是在首次调用的时候计算属性的值,之后就不用重新计算了。

 1 class cached_property(object):
 2     """ A property that is only computed once per instance and then replaces
 3         itself with an ordinary attribute. Deleting the attribute resets the
 4         property. """
 5
 6     def __init__(self, func):
 7         update_wrapper(self, func)
 8         self.func = func
 9
10     def __get__(self, obj, cls):
11         if obj is None: return self
12         value = obj.__dict__[self.func.__name__] = self.func(obj)
13         return value

  如果一时不能理解上面的代码,可以参见这篇文章《python属性查找》。bottle中这个例子,前提是这个属性一旦计算了就不会再重新计算,如果应用场景在某些情况下需要重新计算呢?那么可以增加这么一个函数:

1     def set_property_dirty(self, property_name):
2         self.__dict__.pop(property_name, None)

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

  

 1 import functools, time
 2 class cached_property(object):
 3     """ A property that is only computed once per instance and then replaces
 4         itself with an ordinary attribute. Deleting the attribute resets the
 5         property. """
 6
 7     def __init__(self, func):
 8         functools.update_wrapper(self, func)
 9         self.func = func
10
11     def __get__(self, obj, cls):
12         if obj is None: return self
13         value = obj.__dict__[self.func.__name__] = self.func(obj)
14         return value
15
16 class TestClz(object):
17     @cached_property
18     def complex_calc(self):
19         print ‘very complex_calc‘
20         return sum(range(100))
21
22     def __set_property_dirty(self, property_name = ‘complex_calc‘):
23         self.__dict__.pop(property_name, None)
24
25     def some_action_effect_property(self):
26         self.__set_property_dirty()
27
28
29
30 if __name__==‘__main__‘:
31     t = TestClz()
32     print ‘>>> first call‘
33     print t.complex_calc
34     print ‘>>> second call‘
35     print t.complex_calc
36     print ‘>>>third call‘
37     t.some_action_effect_property()
38     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:

http://gpp.tkchu.me/dirty-flag.html

http://gameprogrammingpatterns.com/dirty-flag.html

https://gpgroup13.wordpress.com/

http://www.knockmeout.net/2011/05/creating-smart-dirty-flag-in-knockoutjs.html

http://www.cnblogs.com/oneplace/p/5833142.html

时间: 2024-10-05 04:09:30

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

【游戏设计模式】之四 《游戏编程模式》读书笔记:全书内容梗概总结

本系列文章由@浅墨_毛星云 出品,转载请注明出处.   文章链接:http://blog.csdn.net/poem_qianmo/article/details/53240330 作者:毛星云(浅墨)    微博:http://weibo.com/u/1723155442 本文的Github版本:QianMo/Reading-Notes/<游戏编程模式>读书笔记 这是一篇超过万字读书笔记,总结了<游戏编程模式>一书中所有章节与内容的知识梗概. 我们知道,游戏行业其实一直很缺一本系

《Java Bug模式》读书笔记

Bug模式是程序中已发生的bug和潜在bug之间重复出现的相互关系.有了这些模式和bug现象的知识,程序员就能很快识别新发生的bug,还可以预防这些bug的发生. Bug模式与反模式有关,反模式是指被多次证明是失败的软件通用设计模式.这些设计的反面示例是传统正面设计模式的必要补充.虽然反模式也是一种设计模式,但Bug模式却是一种和编程错误相关的不正确的程序行为模式.这种关系与设计毫不相关,但是与编写代码和调试过程有关. 下面分别对13中Bug模式进行详细了解. 1.Rogue Tile模式 起因

Redis server命令

欢迎大家加入 459479177QQ群进行交流 本章介绍Redis sever 1.sync,类似MySQL的replication 127.0.0.1:6379> sync Entering slave output mode...  (press Ctrl-C to quit) SYNC with master, discarding 601 bytes of bulk transfer... SYNC done. Logging commands from master. "PING

redis 从0 到 1 键值相关命令 服务器相关命令

keys * 获取所有的key   忽略其数据类型 数据为空   返回(empty list or set) keys a* .*b 获取以a开头 或者 以b结尾的key 返回(empty list or set) exists key 判断key是否存在   存在返回1  不存在返回0 del key 删除key   返回 受影响key的个数 expire key seconds  设置key的过期时间 单位为秒 persist  key   消除key的过期时间设置 move key db

Unity ShaderLab学习总结

Why Bothers? 为什么已经有ShaderForge这种可视化Shader编辑器.为什么Asset Store已经有那么多炫酷的Shader组件可下载,还是有必要学些Shader的编写? 2014-0718-1607-11-33.png 因为上面这些Shader工具/组件最终都是以Shader文件的形式而存在. 需要开发人员/技术美术有能力对Shader进行功能分析.效率评估.选择.优化.兼容.甚至是Debug. 对于特殊的需求,可能还是直接编写Shader比较实际.高效. 总之,Sha

自学总结redis第二部分(redis常用命令、高级命令特性以及与java代码的结合)

六.redis多数据类型介绍(常用命令) 6.1前提操作 #如果前面的redis环境没搭好,那么可以先暂时在 "http://try.redis.io/"中实践redis命令部分.   #为了测试方便,把redis登录密码暂时撤销   #redis一共分为五种基本数据类型:String,Hash,List,Set,ZSet #所有命令都可以到"http://www.redis.cn/commands.html"  去搜索到. #首先由于redis是一个基于key-v

fatfs源码阅读

使用fatfs文件的第一步,就是调用F_mount函数注册一个工作空间. F_mount函数的原型如下: 第一个参数根据网上大神的答复,是外设类型,如果是sd卡就是0,flash等等其他的外设就是其他得数,据说有定义,不过我没找到. 第二个参数FATFS指针就是工作空间的指针,个人感觉有点lwip网卡数据结构的感觉. FATFS数据结构及解释如下,个人感觉了解FATFS这个工作空间数据结构是什么东西就行: typedef struct {    BYTE    fs_type;      /* 

cocod2d-x 之 CCDirector、CCScene、CCSprite

CCDirector是控制游戏流程的主要组件. 1 typedef enum { 2 /// sets a 2D projection (orthogonal projection)2D投机模式 3 kCCDirectorProjection2D, 4 5 /// sets a 3D projection with a fovy=60, znear=0.5f and zfar=1500.3D投影 6 kCCDirectorProjection3D, 7 8 /// it calls "updat

查看Redis信息和状态

原文转自:http://redisdoc.com/server/info.html INFO [section] 以一种易于解释(parse)且易于阅读的格式,返回关于 Redis 服务器的各种信息和统计数值. 通过给定可选的参数 section ,可以让命令只返回某一部分的信息: server 部分记录了 Redis 服务器的信息,它包含以下域: redis_version : Redis 服务器版本 redis_git_sha1 : Git SHA1 redis_git_dirty : Gi