Demystifying EXTENDED_ARG

Recently I was studying Python bytecode for my personal project. One particular thing that bothered me was EXTENDED_ARG. Guess what, the documentation is outdated(or, you could say, wrong), thus causing my confusion. But even without the error, it is not easy to understand at first glance. In this article, I'll explain it in detail.

Early warning: If you've never heard of Python's bytecode, you probably want to learn about it before continue reading.

Before Python 3.6

Let's first take a look at the *original* documentation.

EXTENDED_ARG(ext)

Prefixes any opcode which has an argument too big to fit into the default two bytes. ext holds two additional bytes which, taken together with the subsequent opcode’s argument, comprise a four-byte argument, ext being the two most-significant bytes.

Before Python 3.6, each instruction takes 1 or 3 bytes, depending on whether it takes an argument. Example:

RETURN_VALUE             # opcode takes 1 byte, total is 1
LOAD_CONST     0x0000    # argument takes 2 bytes, total is 3

But what if you want to have a larger argument which cannot fit into 2 bytes? That's when EXTENDED_ARG comes into play. Let's say, the argument is 0x123456

LOAD_CONST     0x123456  # INVALID!! argument exceeds size limit
##################################################
EXTENDED_ARG   0x0012
LOAD_CONST     0x3456    # valid

Here's what Python does: it splits the argument into two parts, with 2 bytes each. The most significant 2 bytes 0x0012 becomes argument of EXTENDED_ARG, the remaining 2 bytes becomes argument of LOAD_CONST. When Python virtual machine sees EXTENDED_ARG, it knows to read the next instruction, and adds their arguments together. So the actual operation Python does is LOAD_CONST with argument 0x00123456.

After Python 3.6

Python 3.6 changed the size of instructions. So starting from Python 3.6, every instruction takes 2 bytes, where opcode still takes 1 byte, argument also takes 1 byte now. If there's no argument, it's just zero.

So what's the equivalent bytecode representation of LOAD_CONST 0x123456 in the latest version of Python? It's like this:

EXTENDED_ARG   0x12
EXTENDED_ARG   0x34
LOAD_CONST     0x56

The change is pretty straight forward, the only difference is the use of multiple EXTENDED_ARG. The size limit for argument is 4 bytes, so there will be 3 EXTENDED_ARG instructions at most, for each subsequent instruction(in this example, LOAD_CONST).

Wait, the documentation is wrong?

Funny enough, the documentation was not updated to reflect this change(which is understandable given the amount of work needed to be done). So I made a PR to fix it.

Play with it yourself

Finally, some code to prove the things we talked about.

I'll be using a brilliant library made by Victor Stinner, called bytecode. Thanks @thautwarm who introduced it to me.

Complete program can be found here.

In this program, I constructs a series of instructions by hand, which does two things: LOAD_CONST 0x1234567 , and then RETURN_VALUE to pop up the loaded value from VM stack. With the help of bytecode lib, things become really simple.

from bytecode import ConcreteInstr, ConcreteBytecode

CONST_ARG = 0x1234567  # The real argument we want to set.
cbc = bytecode.ConcreteBytecode()
cbc.consts = [None] * (CONST_ARG + 1)  # Make sure co_consts is big enough.
cbc.consts[CONST_ARG] = "foo"  # Sets the value we want to load.

if sys.version_info >= (3, 6):
    cbc.extend([
        ConcreteInstr("EXTENDED_ARG", 0x1),
        ConcreteInstr("EXTENDED_ARG", 0x23),
        ConcreteInstr("EXTENDED_ARG", 0x45),
        ConcreteInstr("LOAD_CONST", 0x67),
        ConcreteInstr("RETURN_VALUE"),
    ])

cbc.extend manually constructs the instructions, and the instructions should look familiar now. The tricky thing is about preparing value. Since we want to call LOAD_CONST 0x1234567, there needs to be a value located at co_consts[0x1234567](If you don't know what co_consts is, check out the doc). So what we do is set the value manually: cbc.consts[0x1234567] = "foo".

Now comes the interesting part, let's dis the code object we just created:

  1           0 EXTENDED_ARG             1
              2 EXTENDED_ARG           291
              4 EXTENDED_ARG         74565
              6 LOAD_CONST           19088743 ('foo')
              8 RETURN_VALUE

Is something wrong? Why is the argument different from what we set? Here's how it works

1 = 0x01
291 = 0x0123
74565 = 0x012345
19088743 = 0x01234567

Now it's clear, Python accumulates the arguments when seeing EXTENDED_ARG, and it becomes Instruction.arg. But under the hood, the byte value is still exactly what we set.

for raw_byte in code.co_code:
    print("raw code is: ", raw_byte)

"""
raw code is:  144   # EXTENDED_ARG
raw code is:  1     # 0x01
raw code is:  144   # EXTENDED_ARG
raw code is:  35    # 0x23
raw code is:  144   # EXTENDED_ARG
raw code is:  69    # 0x45
raw code is:  100   # LOAD_CONST
raw code is:  103   # 0x67
raw code is:  83    # RETURN_VALUE
raw code is:  0     # no argument, so zero
"""

The program also supports running with versions before 3.6, the only difference is the two bytes argument:

cbc.extend([
    ConcreteInstr("EXTENDED_ARG", 0x123),
    ConcreteInstr("LOAD_CONST", 0x4567),
    ConcreteInstr("RETURN_VALUE"),
])

个人音乐存储,终极解决方案

2019.10.13 Update:还是需要更新一下使用体验,因为我发现这个方案有两个致命缺陷。

  • 播放列表能分享的部分只有 500 首,也就是说不管原来的列表多大,都只能分享前 500 首。
  • 播放器体验不好。比如,没有办法直接随机播放,一定要先播放第一首,然后才随机。

综上,寻找音乐存储解决方案的旅程看起来还会继续下去。。。

原文

多年以来,我一直在寻找满意的的音乐解决方案。三年前写过一篇文章《可以好好听音乐了》,从那时起我就不再依赖云平台。当时正好有一台实验室的服务器,于是我自己架设了 Subsonic 作为 streaming server。随着毕业,我把所有音乐搬到了 Google Play Music。我会把歌下下来,传到 Google Play Music,这是我近两年来听音乐的方式。

我满意 Google Play Music 的大部分功能,UI 算不上好但够用了,自动匹配封面也不错。唯一唯一的缺点,就是无法分享歌曲/播放列表。确切地说,分享功能是有的,但只有那些 Google Play Music 拥有版权的歌曲才对你的朋友可见。

最近我实在不想继续将就,遂又开始找新平台。这次试遍了市面上几乎所有服务,最终选定了 pCloud。先来说说我的需求吧:

  1. 能上传音乐并收听
  2. 无需自行架设服务
  3. 无需为流量付费
  4. Win、Mac、移动、网页版支持
  5. 不会移除、替换音乐
  6. 能分享播放列表

其它平台有哪些不足呢?

先说国内御三家,QQ、网易、虾米。QQ 和虾米无法上传,直接否定。网易倒是有个音乐云盘,但不知道怎么回事 Mac 版无法使用(有入口但点了没效果)。而且据说网易云会自动替换无版权音乐,这是我绝对无法接受的。

国外有什么呢?Spotify 用的最多,但它也没有上传功能,并且得付费。我还试了一大堆别的,都不满足需求。值得一提的是 Sound Cloud,我很喜欢它的 UI,上传分享功能也都有,问题和是它会自动移除检测到无版权的歌曲。

Streaming server 倒是很多,但是我不想自己维护,所以也不行。

再来就是云盘了,Google Drive 对音乐支持极差,百度网盘我不信任。Dropbox 据说还行,但是免费套餐只有 2GB 太少了。

然后我搜到一篇文章:Best Cloud Storage for Music 2019,其中推荐了 pCloud,正好我之前也偶尔用,就试了试。没想到居然完全满足需求。

播放器长这样:

不好看,但是够用了。

播放列表长这样:

嘛,普普通通。

默认 10G 空间暂时够用,再说我并不介意合理付费。

最关键的是,它能分享啊

laike9m's playlist

分享播放列表是我多年来的一个心愿。这个列表不是那种神曲选集,是纯按个人口味挑选的,当然其中也有很多大家熟悉的歌。类别的话基本就是 Anisong + 各种OST + 一些英文歌。这个列表我维护很多年了,未来还会持续更新。

最后欢迎大家给我推荐歌曲。那种特别热门的就不必了,我多半已经听过了。

People Die, but Long Live GitHub

不知道有没有人注意到,Joe Armstrong 最近几个月都在忙着迁移博客到 TiddlyWiki。我很早就关注了他的 Twitter,然而之前并没多想。听闻大师在 4 月 20 号去世,我才反应过来,原来他之前的举动是在未雨绸缪。

TiddlyWiki 是个单文件的 Wiki 系统,但这并不重要。重要的是,你把信息存在哪?如果你希望存储一段信息,让 100 年后的人也能访问,要怎么做?

  • Facebook, Twitter, 微博?不要说 100 年,我都怀疑 20 年后它们还在不在;
  • Google Cloud, Amazon, 阿里云 + 个人域名?他们大概会存在地更久一些。但没有人能保证不出事故,比如之前的腾讯云事件,比如万一服务器被入侵,黑客把文件删了。还有,你怎么让信息能一直被访问?域名会过期。到某个时候,跑着旧版本操作系统的机器也可能被强制下线或升级,你的服务一定能够在新版本正常运行吗?
  • Dropbox, Google Drive, 百度网盘。同样地,这就要看你信不信这些公司一百年后还存在了,哦不对,他们即使还在,服务也可能早关了(望向 Google
  • Wiki。Wiki 很好,但并不适合存储个人信息,且可能被删改。
  • 去中心化存储,比如区块链。说实话,我对区块链了解有限,但直觉上,我怀疑它能否帮助我们达成目标。对比另一个去中心化的例子:BT。当你要下一些老动画或者电影的时候,拖不下来是常事,因为"死种"了。这才几年呢。当然或许区块链有某种神奇的魔法可以解决这个问题?欢迎了解的朋友们补充。

所以,我们还有什么选择?想来想去,也只有 GitHub 了。

GitHub 已经成为互联网最重要的基础设施之一,有太多人,太多事都直接或间接地依赖于 GitHub,除非人类在未来完全不需要开源代码(这显然不可能),否则我想不出 GitHub 有关闭的可能。对 GitHub 来说,存在 100 年简直是小意思,500 年也不是不可能。这是我的预言,不一定准确,但我还挺有信心的。

不管怎么说,对我们的目标 100 年来说,GitHub 完全可以胜任。除了服务本身的持久性,GitHub 还有两个独特优势:

  • Git。Git 能保存所有历史。
  • Fork。就算黑客黑进了一个账户,删掉 repo,他能把所有 fork 都删干净吗?

综上所述,目前来看,我认为 GitHub 是在百年尺度上存储信息并让其能被访问的唯一途径。未来也许会有 option 2, option 3 出现,但 GitHub 作为 option 1 依然会存在。我相信,Joe 用 GitHub 来 host 博客绝不是突发奇想,他一定是在了解自己的身体状况的前提下,思考了一遍现存所有存储方式,然后同样发现只有 GitHub 才能满足需求。

人总想留下某种痕迹,证明自己活过,然而事实上,99.999% 的普通人就这么被历史遗忘了——曾经是这样。我们处在信息时代的早期,同样也处在人类文明的早期。从今往后,被数字化的东西只会越来越多。既然有人意识到了 GitHub 的独特性,随着时间推移,越来越多的人总会意识到。那时候会发生什么?自然是,越来越多的人会把自己的信息搬到 GitHub 上,依托 GitHub 实现曾经人们可望而不可及的"永生"。人有两次死亡,第一次是肉体,第二次是被人忘记。我忘记这句话是谁说的了,但现在我们已经可以回避第二次。只要 GitHub 支持,就一定会有人这么做,至少我是其中之一。几十几百年后,GitHub 将成为世界上最大的数字公墓,注册用户大部分都已去世,然而个人主页,项目,commit 历史 还述说着他们生前做过的事——就比如 Joe 的博客。这虽然是个比较 creepy 的推论,但从另一个角度想,却证明了人类的巨大进步:对抗死亡是人类文明的永恒主题,而我们已经实现了阶段性胜利。现在是文章、照片、视频,也许还有以个人习惯作为输入训练的模型。再往后呢?会不会有基因信息,乃至意识的完整复制呢?依托于稳定的存储,我们能做的事情实在太多了。反例是现在的一些 Memorial Websites,他们把逝者的信息放在自己网站上供亲友吊唁。这不能说没用,但在我看来去使用这类服务实在有些草率——就算他们再信誓旦旦,也摆脱不了某一天关站的风险,那时候还指望他们好好管理这些数据?没可能的。

既然 GitHub 变成数字公墓是一种必然,我对他们的唯一希望,就是保持某种道德义务。我完全可以想象某一天他们出台一个政策,把二十年内没有活动迹象的账户全部 archive,GitHub Pages 全部下线。那就实在太恐怖了。我祈祷那一天不会到来。


top