• 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:
  • 相关阅读:
    机器学习-分类算法-决策树,随机森林10
    机器学习-分类算法-模型选择与调优09
    机器学习-分类算法-精确率和召回率08
    机器学习-分类算法-朴素贝叶斯算法07
    机器学习-分类算法-K-近邻算法06
    java读取自定义配置文件并引用
    kafka发布消息报错LEADER_NOT_AVAILABLE
    Kettle位置参数(Argument)、命名参数(Parameter)、变量(Variable)
    kettle里的两个参数和一个变量
    如何在命令行下运行kettle的作业(job)和转换(transform)
  • 原文地址:https://www.cnblogs.com/xybaby/p/6399499.html
Copyright © 2020-2023  润新知