Blog

识解实验的金钱版本

Content #

行为经济学家理查德·塞勒(Richard Thaler)所领导的一次经典实验,堪称这一视觉错觉现象的金钱版本。我们和阿努伊·沙阿一起重新进行了这场实验。我们请实验对象思考以下两个场景,它们的区别只存在于括号中的文字——一个场景是超市,另一个场景是度假酒店。

请想象在一个炎热的日子里,你正躺在沙滩上。你能喝到的只有冰水。最后一个小时,你特别想喝一瓶自己最喜欢的冰啤酒。一位朋友起身去打电话,提出可以从附近唯一能买到啤酒的地方带酒回来(这是一个简陋的小超市)(这是一家高档的度假酒店)。朋友说,啤酒可能有点贵,问你愿意为啤酒付多少钱。他说,如果啤酒的价格与你所说的价格一样或低一些,他就会买;但如果啤酒的价格高于你说的价格,他就不会买。你信任这位朋友,而且也确实不存在与酒保讨价还价的可能性。此时,你会告诉他什么样的价格?

与塞勒最初的实验报告一样,富有的实验对象表现出了经典的决策偏见。在高档度假酒店中,他们会为同一瓶啤酒支付更高的价格。与亚历克斯的行为相同,支付意愿的差异也是一种自相矛盾。一瓶啤酒就是一瓶啤酒(而且他们也会在同样的沙滩上享用同样的啤酒),无论啤酒是来自简陋的小超市还是高档的度假酒店,都同样能解渴。而富有的人并不清楚应该为之支付多少钱,所以只能利用环境来做判断。

穷人的表现就非常不一样了。在两个环境中,他们愿意支付的价格非常接近。关键不在于他们愿意支付价格的高低,而在于他们给出了更加统一的价格。请注意,我们向实验对象询问的并不是他们预期支付的价格。向实验对象提问时,无论是穷人还是富人都会给出同样的答案:高档度假酒店中啤酒的价格一定会更高。两组实验对象的不同之处只在于,他们愿意支付的价格变低了。实验结果与我们的预测相符:穷人对所支付的金钱有着更加清晰的认识,他们不会为环境所动,而是会依赖于自身对金钱价值的内化衡量尺度做出判断。

Viewpoint #

仍旧像是相对性原理的应用。

From #

《稀缺》 塞德希尔·穆来纳森 2014

识解(Construal)实验

识解(Construal)实验

Content #

识解(Construal) 在认知过程中,大脑会利用大量的环境线索去理解视觉数据。

在认知过程中,大脑会利用大量的环境线索去理解视觉数据。我们只要了解了大脑所利用的线索,就能予以操控,但有时这种操控甚至可能会引发反常的结果。由麻省理工学院的泰德·阿德尔森(Ted Adelson)所设计的方块阴影错觉图充分利用了这一知识,也就是我们常用的视觉错觉展示图:

这幅图会给人以错觉,方块A的颜色看起来比方块B要深一些。之所以称之为错觉,是因为方块A和方块B的灰度是完全相同的。你可能不相信,就连我们有时也觉得需要再去仔细查一下两个方块的颜色,因为方块A和方块B看起来真的不一样。如果你不想轻信我们的这个说法,就请拿出一张纸,盖在图上剪两个洞——只露出方块A和方块B。这样你就能看到,这两个方块的颜色完全一致。那么,我们的双眼为什么会遭遇如此的误导?

一般来说,视觉系统会利用图像中的背景线索为物体赋予意义,而背景线索则会影响前景物体在人们视野中的呈现。我们可以看到,方块B的背景与方块A不同:方块B的周围都是颜色更深的方块,而且正好位于圆柱体的阴影之中。因为阴影中的物体看起来颜色更深,所以我们的眼睛就会针对阴影进行修正,让物体看起来颜色更浅。可见,感知颜色与感知距离一样,取决于背景环境。感知价值也是同理。

Viewpoint #

From #

《稀缺》 塞德希尔·穆来纳森 2014

GIL工作原理

GIL工作原理 #

下面这张图,就是一个 GIL 在 Python 程序的工作示例。其中,Thread 1、2、 3 轮流执行,每一个线程在开始执行时,都会锁住 GIL,以阻止别的线程执行;同样的,每一个线程执行完一段后,会释放 GIL,以允许别的线程开始利用资源。

细心的你可能会发现一个问题:为什么 Python 线程会去主动释放 GIL 呢?毕竟,如果仅仅是要求 Python 线程在开始执行时锁住 GIL,而永远不去释放 GIL,那别的线程就都没有了运行的机会。

没错,CPython 中还有另一个机制,叫做 check_interval,意思是 CPython 解释器会去轮询检查线程 GIL 的锁住情况。每隔一段时间,Python 解释器就会强制当前线程去释放 GIL,这样别的线程才能有执行的机会。

不同版本的 Python 中,check interval 的实现方式并不一样。早期的 Python 是 100 个 ticks,大致对应了 1000 个 bytecodes;而 Python 3 以后, interval 是 15 毫秒。当然,我们不必细究具体多久会强制释放 GIL,这不应该成为我们程序设计的依赖条件,我们只需明白,CPython 解释器会在一个“合理”的时间范围内释放 GIL 就可以了。

Viewpoint #

From #

23 | 你真的懂Python GIL(全局解释器锁)吗?

CPython引入GIL的原因

CPython引入GIL的原因 #

CPython 使用引用计数来管理内存,所有 Python 脚本中创建的实例,都会有一个引用计数,来记录有多少个指针指向它。当引用计数只有 0 时,则会自动释放内存。

什么意思呢?我们来看下面这个例子:

>>> import sys
>>> a = []
>>> b = a
>>> sys.getrefcount(a)
3

这个例子中,a 的引用计数是 3,因为有 a、b 和作为参数传递的 getrefcount 这三个地方,都引用了一个空列表。

这样一来,如果有两个 Python 线程同时引用了 a,就会造成引用计数的 race condition,引用计数可能最终只增加 1,这样就会造成内存被污染。因为第一个线程结束时,会把引用计数减少 1,这时可能达到条件释放内存,当第二个线程再试图访问 a 时,就找不到有效的内存了。

所以说,CPython 引进 GIL 其实主要就是这么两个原因:

一是设计者为了规避类似于内存管理这样的复杂的竞争风险问题(race condition);二是因为 CPython 大量使用 C 语言库,但大部分 C 语言库都不是原生线程安全的(线程安全会降低性能和增加复杂度)。

Viewpoint #

From #

23 | 你真的懂Python GIL(全局解释器锁)吗?

Linux宏READ_ONCE和WRITE_ONCE

Linux宏READ_ONCE和WRITE_ONCE #

__READ_ONCE,__WRITE_ONCE 两个宏,如下所示。

#define __READ_ONCE(x)  \
(*(const volatile __unqual_scalar_typeof(x) *)&(x))
#define __WRITE_ONCE(x, val) \
do {*(volatile typeof(x) *)&(x) = (val);} while (0)

__unqual_scalar_typeof表示声明一个非限定的标量类型,非标量类型保持不变。说人话就是返回x变量的类型,这是GCC的功能,typeof只是纯粹返回x的类型。如果 x 是int类型则返回“int”,上面的宏相当于:

#define __READ_ONCE(x)  \
(*(const volatile int *)&(x))
#define __WRITE_ONCE(x, val) \
do {*(volatile int *)&(x) = (val);} while (0)

Linux 定义了 __READ_ONCE,__WRITE_ONCE 这两个宏,是对代码封装并利用 GCC 的特性对代码进行检查,把让错误显现在编译阶段。其中的“volatile int *”是为了提醒编译器:这是对内存地址读写,不要有优化动作,每次都必须强制写入内存或从内存读取。

Viewpoint #

From #

09 | 瞧一瞧Linux:Linux的自旋锁和信号量如何实现?

GCC嵌入汇编代码的语法

Python多线程用法

Python多线程用法 #

下载多个网页的单线程版本:

def download_one(url):
    resp = requests.get(url)
    print('Read {} from {}'.format(len(resp.content), url))
def download_all(sites):
    for site in sites:
        download_one(site)

多线程版本:

def download_one(url):
    resp = requests.get(url)
    print('Read {} from {}'.format(len(resp.content), url))

def download_all(sites):
    with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:
        executor.map(download_one, sites)

当然,我们也可以用并行的方式去提高程序运行效率。你只需要在 download_all() 函数中,做出下面的变化即可:

with futures.ThreadPoolExecutor(workers) as executor
#=>
with futures.ProcessPoolExecutor() as executor:

在需要修改的这部分代码中,函数 ProcessPoolExecutor() 表示创建进程池,使用多个进程并行的执行程序。不过,这里我们通常省略参数 workers,因为系统会自动返回 CPU 的数量作为可以调用的进程数。

Python 中的 Futures 模块,位于 concurrent.futures 和 asyncio 中,它们都表示带有延迟的操作。Futures 会将处于等待状态的操作包裹起来放到队列中,这些操作的状态随时可以查询,当然,它们的结果或是异常,也能够在操作完成后被获取。通常来说,作为用户,我们不用考虑如何去创建 Futures,这些 Futures 底层都会帮我们处理好。我们要做的,实际上是去 schedule 这些 Futures 的执行。比如,Futures 中的 Executor 类,当我们执行 executor.submit(func) 时,它便会安排里面的 func() 函数执行,并返回创建好的 future 实例,以便你之后查询调用。

...

Python中的并发和并行

Python中的并发和并行 #

并发(Concurrency)和并行(Parallelism)这两个术语经常一起使用。

在 Python 中,并发并不是指同一时刻有多个操作(thread、task)同时进行。相反,某个特定的时刻,它只允许有一个操作发生,只不过线程 / 任务之间会互相切换,直到完成。我们来看下面这张图:

图中出现了 thread 和 task 两种切换顺序的不同方式,分别对应 Python 中并发的两种形式——threading 和 asyncio。

对于 threading,操作系统知道每个线程的所有信息,因此它会做主在适当的时候做线程切换。很显然,这样的好处是代码容易书写,因为程序员不需要做任何切换操作的处理;但是切换线程的操作,也有可能出现在一个语句执行的过程中(比如 x += 1),这样就容易出现 race condition 的情况。

而对于 asyncio,主程序想要切换任务时,必须得到此任务可以被切换的通知,这样一来也就可以避免刚刚提到的 race condition 的情况。

至于所谓的并行,指的才是同一时刻、同时发生。Python 中的 multi-processing 便是这个意思,对于 multi-processing,你可以简单地这么理解:比如你的电脑是 6 核处理器,那么在运行程序时,就可以强制 Python 开 6 个进程,同时执行,以加快运行速度,它的原理示意图如下:

对比来看,并发通常应用于 I/O 操作频繁的场景,比如你要从网站上下载多个文件,I/O 操作的时间可能会比 CPU 运行处理的时间长得多。而并行则更多应用于 CPU heavy 的场景,比如 MapReduce 中的并行计算,为了加快运行速度,一般会用多台机器、多个处理器来完成。

Viewpoint #

From #

21 | Python并发编程之Futures

穷人更接近于经济人

Content #

1美元的价值,在穷人眼中和富人眼中是不一样的。环境条件会影响富人对1美元的价值判断。当穷人在评估1美元的价值时,会用上大脑中内化的衡量标准,而不会依赖环境进行判断。穷人是金钱价值方面的专家,他们更接近于“经济人”。人们对货币价值的衡量是相对的,这是行为经济学中的经典结论。

我们所谓的节俭根本派不上用场。我们会花好几个小时在网络上东查西找,就只是为了从一双标价为150美元的鞋子上省出50美元;但我们却不会为了从一辆价值两万美元的汽车上省出几百美元,而花费几个小时的时间去做信息搜集工作。

Viewpoint #

From #

《稀缺》 塞德希尔·穆来纳森 2014

突破认知的相对性

突破认知的相对性

Content #

对于绝大多数人来说,如果能从价格为100美元的DVD播放机中省下50美元,就是很大一笔钱(相当于打了5折),但对于价格1000美元的笔记本电脑来说,这50 美元就显得微不足道了(只有九五折)。相反,慈善食堂的食客们却对这种变化无动于衷。那么,稀缺(这一案例中特指缺钱)为何会对这一规律发挥倒置作用呢?

认知具有高度的相对性。举例来说,人眼对光度的判断与背景的明暗相关:如果你身处黑暗的洞穴中,即使一根火柴的光亮都足够照亮你的四周;如果是在一个阳光明媚的午后,你在户外咖啡厅点燃了同样一根火柴,那就可能根本察觉不到它所发出的光亮。同样,我们所能感知的物体大小也是相对的,在日常生活中常常会有这样的体验。洗衣液生产商很早以前就发现,如果瓶盖变大,洗衣液的用量就会变多。大瓶盖里面的倾倒口只会占整个瓶子的很小一部分,因为我们体会到的是相对量而非绝对量,所以倾倒口就显得很小。就这样,人们每次都会多倒一些洗衣液,而洗衣液的销量也会因此增长。在某种程度上,人们对金钱的判断也是与背景相对的。这就是为什么我们会更在乎为购买一本20元钱的书去节省4 元钱,而不会在乎为购买一台1000元钱的冰箱去节省100元的原因。在印度金奈,亚历克斯不过是用看火柴光亮的眼睛去看待金钱,是在与背景相对比。当公允价格为40卢比时,60卢比看起来的确是太贵了。

虽然相对性认知是大脑处理信息时的固有特征,但经验与专业技能还是能让我们摆脱这一限制。

低收入消费者在很多方面都非常精明。去超市购买薯片或金枪鱼罐头时,人们会自然而然地假定,大包装商品的单价一定更便宜,所以购买大包装商品能省钱。而事实上,这种假定常常是错误的。大包装商品的单位价格有时更高,因为可能存在“大件商品附加费”。一项调查发现,在不同型号包装的品牌商品中,其中 25%的商品存在某种形式的大件商品附加费。这些附加费并非定价失误,《消费者报告》(Consumer Reports)称其为“消费品的卑鄙伎俩”。在那些不留意价格,只是假设大包装商品会更合算的消费者身上,这一伎俩非常奏效。(你是否也经常落入这种圈套?)一项研究对哪些超市善于玩弄这些“花招”进行了调查,结果与我们所讨论的话题不谋而合——位于低收入社区的超市较少出现大件商品附加费。当消费者十分在意花出去的每一元钱时,商家就很难从他们身上榨出什么油水了。

简而言之,穷人是金钱价值方面的专家。他们在评估1元钱的价值时,会用上大脑中内化的衡量标准。他们并不会凭借环境去判断物品值多少钱。需求的压力始终存在于穷人心中,挥之不去,从而造就了他们自身的内化尺度。拥有内部标尺的意义在于,他们不像普通人那样容易受到背景环境的影响,如同音乐家能准确地掌握节奏一样。慈善厨房的食客们不会表现出与亚历克斯在金奈时表现出的同样的偏见,或者那些高收入实验对象表现出的同样的偏见,因为他们不那么容易利用环境中随处获取的特征作为价值衡量标准。

Viewpoint #

经验与专业技能,就能突破相对性原理。

From #

《稀缺》 塞德希尔·穆来纳森 2014

稀缺的粒度

Content #

请想象我们二人——塞德希尔与埃尔德应邀去参加野餐。塞德希尔负责带水果做沙拉,埃尔德则负责带软糖。塞德希尔必须要好好考虑一下如何将水果装进包里,因为他要带一个大西瓜,而他的包中已经装了许多其他水果。在塞进菠萝之后,背包实在装不下其他东西了。此时他想:也许我可以将香蕉掰开,沿着背包的边缘码放整齐,要不就在苹果和梨子的缝隙中塞进一点点葡萄或草莓。将水果装进背包的事情看似简单,实际上却很难找到其最佳的排列方法。与此相比,埃尔德的任务就轻松多了。他将西瓜味的软糖扔进包里,又随手丢了几个橙子口味的。之后他拿起背包晃了晃,背包中瞬间就又多出了一些空间。于是,他又在包里装了其他几种口味的软糖。

埃尔德也许同样面临着权衡问题,那就是他可能无法将所有口味的软糖都装上。但只要做出了选择,他的打包行动从本质上讲就会比塞德希尔简单许多——装软糖这件事情根本不需要开动脑筋来想办法。将塞德希尔和埃尔德的任务区分开来的,就是物品的“粒度”(granularity)。水果个头相对较大,而软糖则小了许多,像沙粒一样可以随意堆积。随着物品的粒度增大,打包的复杂程度也在提高。

生活中,你是在往行李箱里装小物件还是大物件,这取决于你可支配的资金。如果你拥有的资金较少,那么一部iPod就会让你感觉到贵重,因为它可能会占据你整月开销的一大部分。而随着资金的增加,一部iPod所占的比例也会变得越来越小。在你可支配的收入中,购买iPod的资金会变得越来越微不足道。可支配资金越多,就越能令决策不那么容易引发直接后果,而且也能降低打包与装箱的复杂性。当可支配空间变少时,装箱的物件体积就会变得相对较大,装箱难度也会相应增加;而当可支配空间变多时,装箱物件的体积就会相对变小,装箱难度也自然会减轻。

Viewpoint #

粒度大的事情分割成粒度小的事情,安排起来就会更加灵活。

From #

《稀缺》 塞德希尔·穆来纳森 2014