Stelpolva Write

Reader

Read the latest posts from Stelpolva Write.

from senioria

Well... Since the title was in English...

Let's skip all the common preambles illustrating the importance, etc. of partial evaluation, and just say we want to write a partial evaluator for a procedural language, which Senioria didn't find any articles introducing many details. Note that this is only a personal note, and the algorithm here may not be the optimal :)

The language has the following characteristics:

  • Functions don't take closures, or, in other words, closures are passed to functions as an implicit argument;
  • Instructions are grouped in basic blocks, thus all branch instructions may only occur at the end of a basic block, and each block is terminated with a branch instruction.

Concepts and basic partial evaluation

We all know how to write an interpreter, right? And yes, this is how a partial evaluator works: just evaluate the instructions, record all variables (here, the results of each instruction), and generate corresponding instructions when the result of the instruction may only be known at runtime, i.e. the instruction must be generated. Other optimizations may also apply, like pattern match the instruction or a sequence of instructions and replace them with a more efficient version, or postpone the instruction generation, and only write the values used by the control flow and side effect-generating instructions into the result (specialized) function to do dead code elimination.

Here, we call the dictionary recording all variables “poly”, which is an abbreviation of polyvariant, which... means that multiple versions of the function is generated, or more precisely, specialized. Not a good name, frankly.

Let's see an example of our partial evaluator at this point, for the following function:

int f(int n) {
entry:
  int i = 5;
  goto if_cond
if_cond:
  bool _0 = i < n;
  if _0 goto if_then else goto if_else;
if_then:
  println("lt");
  int i1 = i + 1;
  goto if_end;
if_else:
  println("ge");
  int i1 = i + 2;
  goto if_end;
if_end:
  int i2 = i1 + n;
  return i2;
}

With n = 1, our partial evaluator would be working like this:

int f_1() {  // Specializing the function, poly: { n = 1 }
entry:
  int i = 5;  // poly: { n = 1, i = 5 }
  goto if_cond;  // poly unchanged, emit the jump, see our introduction below
if_cond:
  bool _0 = false;  // poly: { n = 1, i = 5, _0 = false }
  goto if_else;  // poly unchanged, since the result of the branch is known, just jump to the desired block
if_else:
  println("ge");  // (Same as above, idem for the followings (x)
  int i1 = 7;
  goto if_end;
if_end:
  int i1 = 8;
  return 8;
}

If this is not good enough, by not emitting the constant assignments and a pass that we would introduce later, the resulting function would be like:

int f_1() {
entry:
  println("ge");
  return 8;
}

And with n unknown, the result would be like:

int f(int n) {  // poly: { n = (arg) }
entry:
  int i = 5;  // poly: { i = 5, n = (arg) }
  goto if_cond
if_cond:
  bool _0 = i < n;
  if _0 goto if_then else goto if_else;  // Unable to determine, generate both branches
if_then:
  println("lt");
  int i1 = i + n;
  goto if_end_1;
if_else:
  println("ge");
  int i1 = i + 2;
  goto if_end_2;
if_end_1:
  int i2 = i1 + n;
  return i2;
if_end_2:
  int i2 = i1 + n;
  return i2;
}

Here we choose to keep the unconditional jumps, since they can be eliminated easily by adding another pass pasting the target block to the position of the jump and eliminating all blocks not referenced by any jumps after pasting —– a pass requiring almost no analysis but translating natural language. And by keeping the blocks, we may eliminate redundant specializations by only emitting the jump and not performing the evaluation when we found ourselves jumping to a same block with the same poly, or we may record the source block in the poly, thus we can directly say “when we found we are jumping to the same poly”.

In the example with unknown n, we generated two different blocks for the same source block if_end. This can simplify some analysis since all variables have only one possible value. Some implements would be in favor of keeping the resulting blocks one-to-one to the source blocks as possible, and only record the variables as dynamic, or record all possible values of the variables. We don't like this because this would reduce the optimization potential by losing information by only record variables as dynamic, or be complex by requiring to order the blocks to evaluate all predecessors of a block before evaluating the block. If we want to reduce the generated code size, there are some more analysis we can add, e.g. collect the values that may be used by each block and their successors, and remove all unused variables from the poly, thus we can avoid emitting the same block twice just because an unrelated value in the control flow, or scan up from the bottom of each block and collect some or all reoccurring instruction sequences into one block, depending on how compact we want the code to be.

Loops

At the point, our strategy works well: constant values are propagated to eliminate unused branches and generate appropriate specialized instructions; what's better, many loops are automatically unrolled completely[^1], some loops have dynamic conditions and can't be unrolled completely, but after one iteration, the body finds itself jumping to an evaluated poly, and the evaluator continues happily. But a simple program would drive our evaluator into infinite loop:

[^1]: Though may be overly aggressive, that we may need to add one more pass to reroll the repeating instructions back into a loop, or add some analysis to process big loops differently

int *gv;
int f(int n) {
  for (int i = 0; i < n; ++i)
    gv[i] = 0;
}

As we can see, i changes every iteration, so our evaluator tries to evaluate all the iterations, but the condition is dynamic, which means on the static perspective, there are infinite possible iteration to be generated, Oops.

We may come up with some simple solution: store both the constant value and the instruction returning the value, and ignore the constant value when deduplicating blocks, or only record the instructions. Great, now we are generating gv[0] = 0 for every iteration, or give up unrolling constant loops.

Well... Other partial evaluators seem not to be bothered by the problem, because one of these applies:

  • We just don't know them;
  • They are partial evaluating some language without loops, so they have other problems to face :);
  • They are evaluating some higher-level, structural IR, where they can directly access the “header” of the loop, and determine ad-hoc whether the loop can be unrolled, and use the correct strategy.

After all, in our IR, we should process non-unrollable loops specially.

First we should know whether we are in an non-unrollable loop. Our solution is performing an analysis to obtain all loops and their containing blocks first, then when we encounter a conditional jump, check if we are at the head block of a loop, and if so, pollute the poly to make every variable that is read in the loop and written in both the loop body and before the head block dynamic, and reevaluate the head block with the polluted poly. This way the loop header is adjusted not specialized with values in the first iteration of variables varying in the loop. And since this only applies to conditional jumps, unrollable loops are not affected because their jumps in the header can be specialized to an unconditional jump.

This is enough for code-generation of the loop, since all variables both changed in and read by the loop is correctly set dynamic, and we would only need to repeat the story on the loop once, because when we encounter the header and the body the second time, we are with a different poly, in which there are variables in the loop body, and then after the second time the body is evaluated, the evaluator would recognize the fact that the incoming header block is evaluated in the second iteration, and end the evaluation to the loop.

Forcefully pollute all read and written variables may be rough, and we can do better. There is an instruction called phi, which yields a value depending on from which block the control flow gets to the phi instruction. With this instruction, we can transform the program to SSA form, i.e. all variables are statically written once, by replacing all reassignments with a new variable, and when there are different possible values of a variable, inserting a phi instruction, since they must be from the different block, one block a different value respectively. With SSA form, we can keep the phi instructions, which normally would be eliminated since in both actual evaluation and partial evaluation, the actual control flow can only go along one path, determining the value of the phi instruction, in the reevaluation of the header block, and mark the value as dynamic; since all variables in the SSA form must be assigned before, this can ensure all variables both changed in the loop and used by the loop are set to the correct source —– the phi instruction. This transformation can be more accurate, and may be more universal.

With this strategy, we must memorize the arms of every phi instruction in the header block of the loop, and fill in the correct value when the control flow reenters the loop. And then we found another problem: the poly would be different after evaluating the loop body, so without special process, we would be attempting to create a new header block, and repeat the iteration once; this is OK when we are free to assign, but the first phi instruction would be left with only one value, which is not allowed in some architectures like LLVM. To reuse the header block we just generated, we can record in the poly a path of dynamic loops, push a loop into it when we found ourselves encounter a dynamic loop at the conditional branch, and pop the loops we are leaving at other jumps; when we are going to jump, check if we are jumping to the header of current loop, and if so, fill in the phi arms and reuse the generated block.

That's all of our strategy for partial evaluating non-unrollable loops. This is Senioriae source code handling branches and phis:

void PeInstVisitor::visitPHINode(llvm::PHINode& inst)
{
  if (poly.inloop) {
    auto val = inst.clone();
    auto phiv = llvm::dyn_cast<llvm::PHINode>(val);
    val->setName(inst.getName());
    val->insertInto(poly.insbb, poly.insbb->end());
    PhiPatch patch{ phiv, {} };
    for (size_t i = 0; i < phiv->getNumIncomingValues(); ++i) {
      patch.arms.emplace(phiv->getIncomingBlock(i), phiv->getIncomingValue(i));
    }
    bbphi[poly.insbb].push_back(std::move(patch));
    while (phiv->getNumIncomingValues())
      phiv->removeIncomingValue(unsigned(0), false);
    auto curv = inst.getIncomingValueForBlock(poly.from);
    phiv->addIncoming(poly.get(curv), poly.genfrom);
    poly.set(&inst, val);
  } else
    poly.set(poly.ip, inst.getIncomingValueForBlock(poly.from));
  state.bfsq.push_back(poly.next());
}


void PeInstVisitor::visitBranchInst(llvm::BranchInst& inst)
{
  auto brto = [&](PePoly& poly, llvm::BasicBlock* dst, bool lastjmp = true) {
    auto path = lastjmp
                  ? poly.path
                  : std::make_shared<std::vector<LoopPathItem>>(*poly.path);
    auto src = inst.getParent();
    PePoly res{
      poly.env, poly.base, &*dst->begin(), src, nullptr, poly.insbb, dst, path,
    };
    // Pop the loops we are leaving (or in other words, not jumping into)
    while (!path->empty() && !loops.getLoopFor(path->back().src)->contains(dst))
      path->pop_back();
    // Br to the cond block of the loop
    auto reloop =
      !path->empty() && dst == path->back().src ? path->back().gen : nullptr;
    if (reloop) {
      for (auto& patch : bbphi[reloop]) {
        patch.node->addIncoming(poly.get(patch.arms.at(poly.srcbase)),
                                poly.insbb);
      }
    } else {
      res.env = std::make_shared<llvm::ValueToValueMap>(*poly.env);
      res.base = std::make_shared<llvm::ValueToValueMap>(*poly.env);
    }
    // If new block: create the block
    auto blkit = polybb.find(res);
    if (!reloop && blkit == polybb.end()) {
      res.insbb = llvm::BasicBlock::Create(ctx, dst->getName());
      res.insbb->insertInto(effect);
      polybb.emplace(res, res.insbb);
      bbpoly.emplace(dst, res);
    }
    return std::make_tuple(res,
                           reloop                  ? reloop
                           : blkit != polybb.end() ? blkit->second
                                                   : res.insbb);
  };
  // Get the target bbs
  llvm::BasicBlock* thenbb = inst.getSuccessor(0);
  llvm::BasicBlock* elsebb = nullptr;
  auto cond = inst.isConditional() ? poly.get(inst.getCondition()) : nullptr;
  if (inst.isConditional()) {
    auto cc = cond ? llvm::dyn_cast<llvm::ConstantInt>(cond) : nullptr;
    if (cc && cc->isZero())
      thenbb = nullptr;
    if (!cc || cc->isZero())
      elsebb = inst.getSuccessor(1);
  }
  // Unconditional
  if (thenbb == nullptr || elsebb == nullptr) {
    auto dst = (thenbb != nullptr ? thenbb : elsebb);
    auto [sub, dstbb] = brto(poly, dst);
    llvm::BranchInst::Create(dstbb)->insertInto(poly.insbb, poly.insbb->end());
    if (sub.insbb)
      state.bfsq.push_back(sub);
    return;
  }
  // In loop: switch to loop mode and regenerate the block
  if (loops.isLoopHeader(inst.getParent()) && !poly.inloop) {
    auto next = poly;
    next.env = std::make_shared<llvm::ValueToValueMap>(*poly.env);
    next.ip = &*inst.getParent()->begin();
    next.inloop = true;
    next.path->push_back({ inst.getParent(), next.insbb });
    // Require an rerun
    polybb.emplace(next, next.insbb);
    next.insbb->erase(next.insbb->begin(), next.insbb->end());
    state.bfsq.push_back(next);
    return;
  }
  // Conditional: copy the inst and insert
  auto val = insinst(&inst);
  auto brval = llvm::dyn_cast<llvm::BranchInst>(val);
  auto ping = [&](int idx) {
    auto [sub, dstbb] = brto(poly, inst.getSuccessor(idx), idx == 1);
    brval->setSuccessor(idx, dstbb);
    return sub;
  };
  auto lsub = ping(0);
  auto rsub = ping(1);
  if (lsub.insbb) {
    state.bfsq.push_back(lsub);
  }
  if (rsub.insbb) {
    state.bfsq.push_back(rsub);
  }
}

There are some problems left, like how to obtain the loops and how to transform the program to SSA. Senioria didn't really implement them, since they are already done by llvm, but the algorithms seem to work well.

For loop detection, we can first make the domination graph, a directed graph of blocks with blocks as nodes and jumps in the blocks as edges. Then we can DFS on the graph, maintaining a stack of visited nodes, and when we visit a visited node N, add all nodes in the stack above N into the loop with header N. We can safely assume that a block an only be the header of one node here, since we can't do any better without better specialization technique.

For SSA transformation, we can iterate the blocks in topological order, ignoring edges from a loop body to its header when entering the header, replacing every reassignment with a new variable, and replace succeeding accesses to the assigned variable with the new variable, and create a phi instruction at the beginning of a block for potentially used variables in the block.

Further steps

We haven't discussed how to process functions yet, which is also crucial, and is at least the same complex as processing loops. Well, Senioria am lazy :)

 
Read more...

from Nanako's Thoughts

#StudyAbroad

经济来源

很多时候别人一听我要留学,马上大呼小叫:哇你家这么有钱?!每到这个时候我都大汗淋漓:因为每次对我说这话的人一般家境都比我好多了……

我家经济条件确实是非常差,农村出身、爸妈负债、哥哥负债、房子没有,车也没有,田也没有。大概我爸妈的信念是赤条条到这世界上来,也打算赤条条地走(把我自己逗笑了哈哈哈哈哈)

但是我算幸运的吧,踩中了计算机行业的最后一点点风口,去了大厂打工,以及离职后还做了一段时间外包,所以几年后也攒了一些钱,不多,大概30万。我人生中第一次拥有这么多钱,我都没告诉我爸妈,一个人就决定我要拿着这个钱出国,不管够不够吧,死也死在国外算了。

知道我要去留学以后,重男轻女的外公外婆那边对我很不高兴,指着我骂读书没用,不如赶紧找下一份工作;甚至我小姨对我破口大骂说我连累了我哥,必须拿我这条命去努力赚钱,赚到让我哥早点结婚,才算补偿到位(我内心os:哪来的一家神经病)所以他们一分钱也没出。

我奶奶则对我要去留学特别高兴,虽然她也不知道国外是哪里、到底有多远,但是她觉得我要去读研究生这件事特别光宗耀祖(p.s.我家太穷了,我是我们家族里literally第一个学历读到研究生的人),她就把她所有的积蓄都拿出来给我了TAT 鄙人第一次被这么明显的偏爱,完全手足无措,哭了好几回。这个时候我还思考了一下,难道这就是男宝的普通待遇吗,就是在你想干什么的时候,家里倾尽所有地帮你,不考虑你以后到底还不还得上来……而对我来说,我外公外婆家的态度才是正常态度,根本没指望能得到他们的支持,反正老娘有钱,我要走谁拦得住我呵呵

另外我爷爷奶奶家这边的亲戚还为我这个家族里的第一个研究生搞了个募捐活动,给我募了个一万左右。这就是读书改变命运吧姐妹们🚬难得在南方宗族里享受了一下男宝的至尊待遇!!

总之,最后我申请留学时满打满算,凑了个35万,能搞得这么多也算是意料之外了!

DIY思路

在国外的朋友基本都劝我不要找中介,理由如下:

  1. 中介赚的就是信息差的钱,自己多花点时间找资料也能弥补
  2. 自己的申请自己最上心,中介才不会关心
  3. 我没那么钱浪费(流泪

所以呢,我觉得准备留学时最重要的就是收集资料的能力!以下几个论坛/社区我经常逛:

逛的时候记得遇到有用的信息记录下来,以免自己忘记。另外留学申请也很简单,我的顺序基本是:

谷歌搜xx国家的大学列表 $\rightarrow$ 去官网收集资料 $\rightarrow$ 按照资料准备 $\rightarrow$ 去官网申请

在这里贴一下我当时收集德国大学资料时写的文档(本p人为了省钱真的做到了极致),非常方便好用!按照文档能很清晰地知道自己要准备什么材料、申请什么时候截止。就算不确定是不是缺了什么,也可以马上顺着链接去二次确认。

我的出国材料准备清单

语言准备

为了省钱,语言当然也是自学的!!我考的是托福,最后分数是101(根据我自己的申请经验,上100后的选择会更多,所以我考了两次险之又险地上了100)。阅读和听力我考得还可以,把我贫瘠的口语分和写作分拉了上来……我看了很多一亩三分地上面的托福备考经验贴!!这个论坛真的有很多好东西,非常推荐大家多去挖一挖!

(二次编辑:删除了自己的托福经验,因为我也是拾人牙慧,自己去搜适合自己的学习方法比较好一点)

选择留学国家

明确了自己的留学成本以后,选国家时也比较有方向(毕竟也没啥能选的啊呵呵

我的目标也很明确!就是移民拿身份!所以我当时主要考虑的有以下几个选项:德国、美国和荷兰。这里我只简单介绍一下我对这几个国家的印象,没有特别详细地展开。

德国

由于我的大学跟德国合作非常紧密,甚至所有大一学生都要强制学一年德语,所以当时德国是我的第一选择。德国的移民条件也比较简单:工作满两年(即交满两年养老保险)后可以申请永居。

留学花费

德国最大的优点就是便宜,真的便宜。除了巴登-符腾堡州以外,所有公立大学都不收学费(要收学杂费),并且巴登-符腾堡州的学费是非欧盟学生一学期1500欧元,与其他国家相比简直就是白菜价。详情请看下图这个表(引自在德国留学要花多少钱?

德国学费与学期注册费情况

学期注册费竟然还包括无限次使用当地公共交通系统……这福利我哭了、、、

此外德国的房租也不是很贵,毕竟德国很大,具体要看住在哪个地方,但总体来说就是选择非常多,总有便宜的选法;德国的物价在欧洲内也算比较ok的。根据资料,在德国生活,一个月平均需要842欧(引自Costs of education and living)。

学校申请

咱是去移民的,完全不在乎什么名校不名校的……我当时的精神状态就是:就随便找吧,哪个能上就上,都申申!

申请德国学校时,我发现一个最大的问题就是它们对申请者的专业背景要求真的太高了!比如说柏林工大当时要求本科所修的课中数学领域的课至少要有20学分,我算了算我自己的只有16学分,而我已经毕业了,去哪里补这四个学分呀……我也不太确定能不能上网课来补学分,但这个情况非常普遍,我想申请的很多学校都因为各种各样的课程要求满足不了而被我毙掉。

以及申请德国学校还有一件事情一定要铭记于心:记得准备APS!!!!而且是越早越好,不能拖,这个APS从准备资料到拿到证书花了我大概半年的时间……期间让我错过了很多学校的申请窗口(痛哭捶地)毕竟咱第一次准备留学也没有经验……

美国

可能有人看到我的标题时很疑惑:你这人,不是说只有35万吗,这去个屁的美国啊?

俗话说猫有猫道,鼠有鼠道,留美的人也各有各的道、、、我的留学预算没有多到让我可以爬藤啊、申名校啊之类的,但是也没少到让我只能惊险走线墨西哥……

留学花费与学校申请

这两放在一起写吧,主要是真的没什么选择。像我这样的肯定是只能考虑物美价廉的公立大学了,比如说我曾经考虑的佛罗里达州立大学(FSU)的国际学生学费是一年18,746刀,那么我的预算大概是够两年的学费加一年的生活费(同时还得缩衣节食),不过它的奖学金机会还算比较多,这里有一个官方提供的表格(引自Graduate Student Financial Support Policy and Statistics),可见这所大学的计算机系的助学金获得率是比较高的,能有76.7%,但是数据比较久了,不知道现在是什么情况。

FSU研究生资助情况统计

除了寻找助学金机会,我也有在寻找一些全奖硕士或者是全奖博士的机会,而一般会提供全奖硕士的学校都不是什么特别有名的学校(也没关系吧)。我也较为幸运地找到了一个全奖硕士的机会,也通过了面试,只要申请就能有offer。这个就不多展开了,总之美国的机会真的很多,能拿得到资助,在美国活下来也不是非常难的事,问题就在于美国太难移民了……这也是最重要的,我为什么放弃它的原因。

至于如何寻找这样的机会,推荐多看看一亩三分地或者是领英上的学校教授、业界大小牛,信息网够广的话总能蹲到的。

这是我之前考虑德国和美国时列的表(当时完全没想到自己会半路一拐突然去了荷兰呢哈哈哈哈哈哈哈)

德国与美国对比

荷兰【最终选择】

说实话,最开始没考虑过荷兰……荷兰的费用对我来说还是有点高。但是我考完托福后没啥事干,刚好荷兰申请季还有一周截止,我怀着“有枣没枣打一杆子”的心态申了三所荷兰的学校,最后被其中一所录了。而我考虑到荷兰申请永居的条件也比较简单:合法居留满五年就可以申请永居,所以最后就来了荷兰!

留学花费

荷兰是真的贵啊……吐血了!!

我申上的这所学校,一年学费就接近两万欧,一年生活费要一万欧,所以算下来留学两年差不多要花6万欧,我的钱还差十万左右,但是咱还是来了。我觉得学费是大头,我的钱够两年学费,和差不多一年多一点的生活费,大头已经有了,那我的缺口还不算很难解决,那就来了再说吧!

我个人的开销是每个月大约900欧:房租445欧,保险20欧,话费5欧,食物大约300欧,交通费和出去玩大概100欧。其实有时候我花得比这个还少(是个很节省的小女孩),所以预计了一下我大概能撑到今年十月或者十一月就没钱了(。)

荷兰的问题在于以下几点:

  1. 毕竟是小语种国家,很多工作都需要荷兰语。
  2. 最近几年的政策逐渐排外,非欧盟身份很不方便;比如找兼职的时候有很多家都因为我的非欧盟身份拒绝我。
  3. 物价高,比德国美国都高。我跟美国的朋友聊天,她说她那里(波士顿)的越南河粉一份12刀,我这里的越南河粉一份18欧,呵呵🚬
  4. 交通费贵。想想德国学生免交通费,可恶,好嫉妒!
  5. 国际生有严格的工作时长限制。一周16个小时/暑假三个月全职工作,二选一。
  6. 奖学金少。

学校申请

当时申得晚,又着急,没有做很多荷兰学校的功课。总的来说,荷兰的申请比德国简单很多,虽然也有对专业背景的要求,但没有德国那么严格。

荷兰的大学水平并没有差很多,我觉得完全可以随便选诶。建议如果要申荷兰的学校主要考虑一下学校的位置、学校福利之类的,比如说格罗宁根大学的学生可以免费学习荷兰语(我这个学校就没有!)

另外体感上荷兰是个比较小的国家,所以周末去另一个城市玩也是很稀松平常的事情,不需要太考虑自己所在城市的娱乐水平(我的宗旨:便宜能住就行了)

最重要的一点可能是得提前看好自己想申请的项目的实习政策是什么样的——荷兰的国际学生实习时需要与公司和学校都签一个Nuffic协议,如果你想实习,但是学校政策不支持的话会非常麻烦。

结语

感觉我已经把我自己掏干净了,总的来说DIY真的很简单,预算有限的话绝对不要把钱都浪费在中介上面啊————

有钱的话就算了,有钱者为所欲为🚬

 
Read more...

from 奈芙莲 • 糯可

译者注:

本文是 Michał “rysiek” Woźniak 的博客 Mastodon monoculture problem 的翻译。 source: https://rys.io/en/168.html

对于不太懂文中的名词的人,译者额外做出一些解释:

  • 联邦宇宙:Fediverse,是使用一些协议彼此链接的一系列网站/服务(被称为“实例”)的总称。在联邦宇宙,你在任何一个实例上拥有账号,你就能访问几乎整个联邦宇宙的所有内容——实例有特殊规则的时候除外。
  • 实例:联邦宇宙上的单独一个网站/服务。
  • 实例软件:运行实例使用的软件,比如 Mastodon。用一种实例软件可以搭建很多个实例。
  • ActivityPub协议:一种旨在让不同的实例之间可以互相连接,共通账号的协议。联邦宇宙实例很多使用的就是ActivityPub协议。
  • Mastodon:连接到联邦宇宙的一个微博类社交媒体软件,有类似Twitter的设计。

依照 Michał “rysiek” Woźniak 的原文 (https://rys.io/en/168.html),本文以 CC-BY-SA 4.0 协议发布。您可以自由地共享、演绎本作品,但是必须署名、以相同方式共享 – 如果您再混合、转换或者基于本作品进行创作,您必须基于与原先许可协议相同的许可协议分发您贡献的作品。


Mastodon 非营利组织的首席执行官兼 Mastodon 软件的首席开发者 Eugen Rochko(在联邦宇宙上被称为 Gargron)最近的举动让一些人担心 Mastodon 该软件项目,同时也是该非盈利组织对联邦宇宙的其余部分造成的巨大影响

确实。我们就是应该担心。

到目前为止,联邦宇宙上大多数人都在使用 Mastodon 软件。 截至撰写本文时,最大的实例 mastodon.social 拥有超过 200,000 个活跃帐户。 这意味着这单独一个实例上涵括了整个联邦宇宙的大约 1/10。 更糟糕的是,Mastodon 软件经常被认为是整个社交网络,这掩盖了一个真相: Fediverse 是一个由更多样化的软件们组成的更广泛的系统

现在它就已经产生了糟糕的后果,而且以后可能会更糟。 让我困扰的还有,我以前也见过这样的情况:

正如 OStatus 宇宙所示

几年前,我在联邦宇宙的前身有一个账户。 它主要基于 StatusNet 软件(后来更名为 GNU Social)和 OStatus协议。 最大的实例是 identi.ca ——笔者在那里拥有自己的账户。 同时还有很多其他实例、其他软件项目也实现了 OStatus协议——例如 Friendica

出于本博文的需要,我们将该社交网络称为“OStatus 宇宙”。

与今天的联邦宇宙相比,OStatus 宇宙是微不足道的。 我没有具体的数字,但我的粗略估计是,即使在最活跃的时候,也只有大约100,000到200,000个活跃帐户(如果你有实际数字,请告诉我,我将很乐意更新这篇博客)。 我也没有 identi.ca 上用户数目的确切数字,但我粗略估计它有 10,000 到 20,000 个活跃帐户。

对,刚好也是整个社交网络的 1/10。

OStatus 宇宙虽小但很活跃。我们在上面有讨论、线程回复(threads)和话题标签(hashtags)。 它早在 Mastodon 软件项目实现 Group 的十年前就实现了群组。它有桌面应用程序——我仍然怀念 Choqok 的可用性! 甚至经过一番唠叨后,我还说服了波兰的一个政府部门在那里设立官方办事处。 据我所知,这是最早的政府机构在自由软件驱动的去中心化社交网络上拥有官方账户的例子。

Identipocalypse (identi的末日)

然后有一天,identi.ca 的管理员(也是 StatusNet 软件的原始创建者)Evan Prodromou 决定将其重构为一项新服务,即 pump.io。 他们希望新软件更好、更精简。 他们创建了一个新协议,因为 OStatus 协议有非常现实的限制。

只有一个问题:新协议与 OStatus 宇宙的其它部分不兼容。它把这个社交网络撕碎了。

拥有 identi.ca 帐户的用户与所有其他 OStatus 实例失去了连接。 在其它实例上拥有帐户的人与 identi.ca 上的人失去了联系,并且其中一些人在 OStatus 宇宙中非常受欢迎(听起来很熟悉?..)。

事实证明,如果一个实例占据了整个社交网络的 1/10,那么它就会承载太多的社交联系。尽管的确存在其他实例,但突然间大量活跃用户消失了。一些群组瞬间就变得安安静静。 即使有人在不同的实例上有一个帐户,并且在其他实例上有联系人,很多熟悉的面孔也会消失。 于是此后不久我就停止使用它了。

从笔者的角度来看,就这么一项行为,就使得我们在推广去中心化社交媒体方面至少倒退了五年甚至十年。 identi.ca 的重构不仅在社交关系意义上破坏了 OStatus 宇宙,而且在协议和开发者社区意义上也是。 正如 Pettter,一位 OStatus 资深人士所说:

我认为,这个巨变带来的影响是,它不仅切断了社交联系,还导致了协议变得支离破碎,一次又一次地让重建联合社交网络的基本架构的努力付诸东流。 也许这是他们重新聚集在一起设计 ActivityPub 的必要步骤,但我个人不这么认为。

当然,Evan 完全有权利这样做,毕竟这是他用自己的钱、按照自己的条款无偿经营的一项服务。 但这并不能改变它割裂了OStatus宇宙的事实。

我认为我们需要从这段历史中吸取教训。我们应该担心 mastodon.social 的庞大规模。 我们应该为联邦宇宙上 Mastodon 软件明显的单一文化感到担忧。 我们还应该担心将整个联邦宇宙与“Mastodon”等同起来。

做大的代价

发展到像 mastodon.social 这样的规模,需要付出相当的成本和风险。 这些成本,尤其是那些风险,既针对该实例本身,也针对更广泛的 Fediverse。

Fediverse 上的审核很大程度上以实例为中心。 单个巨大的实例很难有效地管理,特别是如果它开放注册(就像 mastodon.social 目前所做的那样)。 作为直接在官方移动应用程序中推广的旗舰实例,它吸引了大量新注册,其中包括不少不太友好的用户

同时,这也使得其他实例的管理员和版主更难以做出有关 mastodon.social 的审核决定。

如果其它实例的管理员认为 mastodon.social 出于某种原因缺乏管理,他们是否应该静音该实例,甚至将其屏蔽(显然,有些人已经这样做了),代价是让本站的用户无法联络许多在 mastodon.social 有账户的受欢迎的人?或者、他们就应该冒着让自己的社区面临潜在有害行为的风险,保留这种联络的可能性吗?

mastodon.social 的庞大规模使得任何其他实例的管理决策成为了一件大事。 这是某种形式上的特权:“当然,如果您不喜欢我们的管理方式,您可以封掉我们,但如果您实例上的用户无法访问整个联邦宇宙的 1/10,那将是一种耻辱!” 正如 GoToSocial 网站所说

我们也不认为拥有成千上万用户的旗舰实例对联邦宇宙来说很有好处,因为它们会导致中心化,并且很容易变得“太大而不敢屏蔽”。

请注意,我并不是说这种权力动态是有意识地、有目的地利用的! 但不可否认,它是存在的。

作为一个巨大的旗舰实例也意味着 mastodon.social 更有可能成为恶意行为的目标。 例如,在过去几个月中,它多次受到 DDoS 攻击,并且好几次都因为这个而无法访问。 联邦系统的可靠性依赖于消除大的故障点,而 mastodon.social 现在已经是一个巨大的故障点了。

该实例的规模让它成为了一个诱人的攻击目标,这也意味着它需要做出某些艰难的选择。 例如,由于经常成为 DDoS 的目标,它现在由 Fastly 保护。 从隐私角度和互联网基础设施中心化的角度,这是一个问题。 这也是较小的实例完全避免的一个问题,因为它们很小,很少有人会无聊到 DDoS 攻击它们。

(译者注:这方面译者非常存疑,事实上小实例也经常受到随机的DDoS攻击。)

明显的单一文化

虽然联邦宇宙并不完全是单一文化,但它太接近单一文化了,令人感到不舒服。 Mastodon 非营利组织对整个联邦机构有着巨大的影响力。 这让使用社交网络的人、Mastodon 软件和其他实例软件项目的开发人员以及实例管理员感到紧张。

Mastodon 既不是联邦宇宙上唯一的平台软件,也不是第一个。 例如,Friendica 已经存在了十五年了,早在 Mastodon 软件的第一次 git 提交之前就已经存在。一些现在的联邦宇宙中运行着的 Friendica 实例(例如 pirati.ca)在十年前曾是 OStatus 宇宙的一部分!

但是很多人在将整个联邦宇宙称为“Mastodon”,说的跟 Fediverse 上只存在 Mastodon 软件一样。 这导致人们经常要求 Mastodon 实现一些新功能,但其实这些功能已经由其他实例软件实现了。 Calckey 已经有引用嘟文(带评转发)功能了。 Friendica 也早就有线程对话和富文本

将 Mastodon 与整个联邦宇宙等同起来对于 Mastodon 软件开发人员来说也是不利的。 这导致他们面临着被要求实现不完全适合 Mastodon 软件的功能的压力。 或者,有时候他们不得不两群吵吵囔囔的用户打交道,一群人想要某个功能,另一群人又觉得这个功能太大,实现不了。通过清楚地划出一条界限,并引导人们使用可能更适合他们的用例的其他实例软件,许多此类情况可能会更容易处理。

最后,Mastodon 是目前为止(按活跃用户和实例数量衡量)最流行的 ActivityPub 协议实现。 每个实现都有其自己的特性。 随着时间的推移和新功能的实现,Mastodon 的实现可能会进一步偏离严格的规范。 毕竟,这很诱人:如果你怎么弄都是龙头老大,为什么要艰难的去实现标准协议呢?

如果这真的发生了,其他所有实现是否都必须跟随它,从而变得随波逐流,没有对于事实标准的话语权? 这是否会在 Mastodon 软件开发人员和其他实例软件项目的开发人员之间造成更多紧张关系?

“Mastodon 错过了 XX 功能”的最优解并不总是“Mastodon 应该实现 XX 功能”。 通常来说,最好使用更适合特定任务或社区的不同实例软件。或者开发一个扩展协议,允许尽可能多的实例可靠地实现特别流行的功能。

但这只有在每个人都清楚 Mastodon 只是更大的社交网络:联邦宇宙 的一部分的情况下才有效。 而且,对实例软件、单个实例以及移动应用程序而言,我们现在本来就有很多选择。

遗憾的是,这似乎与 Eugen 最近的决定背道而驰:它们打算导向自上而下(不是完全垂直整合,但倾向于垂直整合)的官方 Mastodon 移动应用程序模型,以推广他们最大的 mastodon.social 实例。 在我看来,这很值得担忧。

更好的方式

我想澄清的是,我在这里并不是主张停止 Mastodon 的开发并且从不实现任何新功能。 我也同意注册流程需要比以前更好、更简化,我也同意需要实施大量 UI/UX 更改。 但所有这一切都可以并且理应以提高联邦宇宙弹性的方式进行,而不是破坏它。

必要的更改

我觉得 Mastodon 和联邦宇宙必须要更改的地方是:

  1. 现在关闭 mastodon.social 上的注册 对于联邦宇宙的其他部分来说,这个实例已经太大了,所带来的风险也太大了。
  2. 使用户迁移更加容易,甚至可以跨实例迁移 在 Mastodon 上,个人资料迁移目前仅移动关注者。 您关注的人、收藏夹、屏蔽和隐藏列表都可以手动移动。 帖子和列表无法移动——这对很多人来说都是一个很大的问题,因此他们就被和他们注册的第一个实例绑在了一起。 这并不是无法克服的——笔者已经迁移了两次个人账户,感觉也很不错。但这种阻力还是太大了。 值得庆幸的是,其他一些实例软件项目也正在努力允许帖子迁移。 但这不会是一个快速而简单的解决方案,因为 ActivityPub 的设计使得在实例之间移动帖子变得非常困难。(译注:Sharkey和Firefish实现了这项功能)
  3. 默认情况下,官方应用程序应该随机从一些可信的实例中选一个推荐给新用户注册 至少其中一些实例不应由 Mastodon 非营利组织控制。 理想情况下,某些实例应该运行不同的实例软件,只要它使用兼容的客户端 API。

我能做什么?

作为联邦宇宙的一员,我们可以做以下事情:

  1. 如果您在 mastodon.social 上有帐户,请考虑迁移走 的确这是有点艰难的一大步,但也是你可以做的最直接有助于解决问题的事情。 几年前,我从 mastodon.social 迁移过来,再也没回去过。
  2. 考虑使用基于不同软件项目的实例 越多的人迁移到使用 Mastodon 软件以外的其他实例软件的实例,我们的联邦宇宙就越平衡越有弹性。 例如,我听说很多人都觉得 Calckey 不错。 GoToSocial 看起来也很有趣。
  3. 请记住,联邦宇宙不仅仅是 Mastodon 语言很重要。 当谈论联邦宇宙时,称其为“Mastodon”只会让我上面提到的问题更难处理。
  4. 如果可以的话,支持 Mastodon 官方项目以外的项目 至此,Mastodon软件项目已经拥有众多的贡献者、稳定的开发团队以及足够雄厚的资金,可以安全地持续很长一段时间。这那太棒了!但是,对于其他与联邦宇宙相关的项目,包括独立的移动应用程序或实例软件,它们没有那么受到关注。为了拥有一个多元化、有弹性的联邦宇宙,我们需要确保这些项目也在各个方面得到支持,比如金钱上。

结束语

首先,联邦宇宙是一个比任何中心化的孤岛更有弹性、更长期可行、更安全、更民主化的社交网络。 即使存在 Mastodon 单一文化问题,它仍然不是(并且不可能 )由任何单一公司或个人拥有或控制。 我也觉得它比只是在 cosplay 去中心化的社交网络,比如 BlueSky 是一个更好、更安全的选择。

从某种意义上来说,OStatus宇宙可以说是联邦宇宙的早期版本; 如前所述,当时属于其中的一些实例仍在运行,并且今天已成为联邦宇宙的一部分。 换句话说,联邦宇宙已经存在了十五年了,尽管它受到了严重的伤害,但它仍然在 identi.ca 的灾难中“幸存下来”,同时见证了 Google+ 的诞生和过早的消逝

我确实相信如今的联邦宇宙比 identi.ca 重新部署之前的OStatus宇宙更具弹性。 就用户群而言,联邦宇宙至少要多十倍,有数十个不同的实例软件项目和数以万计的活跃实例。还有一些认真的机构对其未来进行了投资。 我们不应该对我上面写的一切感到恐慌。 但我确实认为我们应该防患于未来。

我不会将恶意归因于 Eugen 最近的行为(比如让官方 Mastodon 应用程序将新人引向 mastodon.social),也不会归因于 Evan 过去的行为(在 pump.io 上重新部署 identi.ca )。 我认为任何人都不应该这样做。 做好这件事很难,我们都在边走边学,并努力利用有限的时间和有限的资源去做到最好。

Evan 后来成为 ActivityPub(联邦宇宙运行的协议)的主要创建者之一。 Eugen 发起了 Mastodon 软件项目,我坚信这个项目让联邦宇宙蓬勃发展到了今天的样子。 我真的很欣赏他们的工作,并认识到如果没有人发表意见,在社交媒体空间中做任何事情都是不可能的。

然而,这并不意味着我们不能仔细思考这些决定,也不应该对此有这些意见。


更新:我(原作者)犯了一个傻傻的错误,mastodon.social 由 Fastly 提供保护,我以为是 CloudFlare。修复了,感谢指出这个错误的人!

更新2:衷心感谢 Jorge Maldonado Ventura 提供了这篇博文的西班牙语翻译,并在 CC BY-SA 4.0 下发布。 谢谢!

 
阅读更多