【四火】 写在在家办公四周之时

几年前我曾经写过一点对于 “在家办公” 的思考,后来又写了一点,但是从来都没有想到过,长期的在家办公如今已经不是一个可选项,而是一个必选项了。

应该说,我从来都不是一个频繁在家办公的支持者,我是说,在家办公这样的互联网公司特有的 “福利” 当然是好的,但是 “频繁” 在家办公对于团队、项目,甚至个人职业成长等等各方面来说,都不是一件好事。我记得在刚加入亚马逊的时候,我听说亚马逊给了一位工程师这样一个 offer——允许一个月之内四分之三的时间远程办公。当时我觉得这件事情还是挺不可思议的,因为像曾经的 37signals 这样的小公司当然容易做到,但是长期和频繁地在家办公却不是大型互联网公司能玩得转的。

现在整个情况都发生变化了,由于新冠疫情,我们被迫在家办公,或者说,全公司绝大多数人都只能选择在家办公了,而且这件事情目前为止还似乎在漆黑的隧道里,还没有看到头。

在家办公的好处

虽然一般情况下,我更希望到公司去办公,而不是在家,但是不可否认在家办公是有很多好处的。

首先,在家办公会让自己更自由。开某些会的时候,可以躺在床上说话,一边说一边闭目休息(比如某些 1 on 1 沟通会);脖子酸了可以趴一会儿;家里有需要帮助和处理的事情也可以很方便地离开一会儿去处理。

其次,在家办公省掉了通勤的时间。不用考虑通勤的时间消耗,就意味着可以把闹铃设到一个更晚的时间,每天可以用于自己支配的时间更充裕。

最后,在家办公也能减少一些打扰。没有人来直接打扰你,这可以算一个原因,但这一条是有争议的,因为既然你在工作,你需要 “在线”,那么你的 Slack 很可能就不会消停……

在家办公的挑战

对于团队来说,面对面沟通和合作所带来的的凝聚力,是在家办公所无法带来的。想象一下,和你团队里的兄弟姐妹做一次 pair coding/review,或者是坐下来喝杯咖啡,再或者打局乒乓球,在 Happy Hour 时间聊一下。现在这些基本上都没有了,有的只有生硬和冷冰冰的一个又一个工作上的会议。

对于项目来说,地理位置上的密切合作,所带来的的沟通高效率,也是在家办公所无法带来的。现在大家似乎都慢慢习惯了对这些远程沟通工具的使用,比如 Zoom 电话会议,可是依然,很多会议还会面临着诸多远程沟通才会有的问题,比如会前调试设备,会中时不时会有杂七杂八的声音,会有人掉线。直接沟通无疑是最高效的,Zoom 这样的工具可以让沟通得以进行,但是看不到手势,看不到表情,连板书也不那么自然,还是损失了很多沟通的信息。

对个人职业发展来说,在家办公也失去了很多能力锻炼的机会。比方说,英语能力本来就不够强大的情况下,面对面的沟通可以化简掉很多的问题,可是电话沟通就会对听说能力要求直接上了一个很大的台阶,保证沟通中信息的传达变成了一件很有挑战性的事情。对我个人来说,已经在西雅图工作好几年了,因此这不算是一个大问题,但是我知道身边有很多初入职场的工程师,英文能力还较为生疏,沟通就更可能成为一个问题。

在家办公的一点技巧

接着聊一点我们可以做的,能够改善在家办公之弊端的措施。

硬件方面,显然,如今的在家办公不是一个 10 分钟的事情,也不是一个一两个钟头的事情,而是长时间,比方说,一天 7、8 个小时都需要持续发生的事情,是一个不能够轻易将就的事情。因此让自己在家办公时自己的身体尽量舒适就显得很有意义。对我来说,我不需要大的显示器,也不需要 laptop 有多高端的配置,但是,人体工学桌椅是很有帮助的,我可以坐一会,站一会。脖子不容易酸,也不容易疲劳。

环境方面,尽可能保证一个安静的房间,是一个很有助益的因素。除此以外,在家的诱惑太多,干扰太多,一个降噪耳机可能能够帮助排除干扰。我有时候也会捧着笔记本电脑,临时地挪到另外的一个更加安静的房间,去开完电话会议。

沟通的技巧方面,我觉得在重要的会议前,简单地罗列一些会议的安排和要点是非常有帮助的,这可能并非特定针对于在家办公的场景,但毫无疑问在家办公让其变得更为重要。开会的时候,有了这样的提纲,就可以专注于要解决的问题,而讨论的时候,也因为有了要点和上下文,效率更高。

最后,还是希望疫情快过去吧。在家呆得已经发霉了,以往生活中喜欢做的事情,比如旅行、打球,或者是看电影,似乎这些不管什么都无法正常进行了。Social distancing 已经让本来就在疏远的人际,越来越远了。

文章未经特殊标明皆为本人原创,未经许可不得用于任何商业用途,转载请保持完整性并注明来源链接 《四火的唠叨》

via 四火的唠叨 https://ift.tt/2JqK1YQ

VN:F [1.9.22_1171]
Rating: 0.0/5 (0 votes cast)
VN:F [1.9.22_1171]
Rating: 0 (from 0 votes)
Share

【喵神】 使用 protocol 和 callAsFunction 改进 Delegate

2018 年 3 月的时候我写过一篇在 Swift 中如何改进 Delegate Pattern 的文章,主要思想是用遮蔽变量 (shadow variable) 声明的方式,来保证 self 变量可以被常时地标记为 weak。本文中,为了保证没有看过原文的读者能处在同一频道,我会先 (再次) 简单介绍一下这种方法。然后,结合 Swift 5.2 的新特性提出一些小的改进方式。

Delegate

简单说,为了避免繁琐老式的 protocol 定义和实现,我们可能更倾向于选择提供闭包的方式完成回调。比如在一个收集用户输入的自定义 view 中,提供一个外部可以设置的函数类型变量 onConfirmInput,并在合适的时候调用它:

class TextInputView: UIView {

    @IBOutlet weak var inputTextField: UITextField!
    var onConfirmInput: ((String?) -> Void)?

    @IBAction func confirmButtonPressed(_ sender: Any) {
        onConfirmInput?(inputTextField.text)
    }
}

TextInputView 的 controller 中,检测 input 确定事件就不需要一堆 textInputView.delegate = selftextInputView(_:didConfirmText:) 之类 的麻烦事了,可以直接设置 onConfirmInput

class ViewController: UIViewController {

    @IBOutlet weak var textLabel: UILabel!

    override func viewDidLoad() {
        super.viewDidLoad()
        let inputView = TextInputView(frame: /*...*/)
        inputView.onConfirmInput = { text in 
            self.textLabel.text = text
        }
        view.addSubview(inputView)
    }
}

但是这引入了一个 retain cycle!TextInputView.onConfirmInput 持有 self,而 self 通过 view 持有 TextInputView 这个 sub view,内存将会无法释放。

当然,解决方法也很简单,我们只需要在设置 onConfirmInput 的时候使用 [weak self] 来将闭包中的 self 换为弱引用即可:

inputView.onConfirmInput = { [weak self] text in
    self?.textLabel.text = text
}

这为使用 onConfirmInput 这样的闭包变量加上了一个前提:你大概率需要将 self 标记为 weak 以避免犯错,否则你将写出一个内存泄漏。这个泄漏无法在编译期间定位,运行时也不会有任何警告或者错误,这类问题也极易带到最终产品中。在开发界有一句话是真理:

如果一个问题可能发生,那么它必然会发生。

一个简单的 Delegate 类型可以解决这个问题:

class Delegate<Input, Output> {
    private var block: ((Input) -> Output?)?
    func delegate<T: AnyObject>(on target: T, block: ((T, Input) -> Output)?) {
        self.block = { [weak target] input in
            guard let target = target else { return nil }
            return block?(target, input)
        }
    }

    func call(_ input: Input) -> Output? {
        return block?(input)
    }
}

通过设置 block 时就将 target (通常是 self) 做 weak 化处理,并且在调用 block 时提供一个 weak 后的 target 的变量,就可以保证在调用侧不会意外地持有 target。举个例子,上面的 TextInputView 可以重写为:

class TextInputView: UIView {
    //...
    let onConfirmInput = Delegate<String?, Void>()
    
    @IBAction func confirmButtonPressed(_ sender: Any) {
        onConfirmInput.call(inputTextField.text)
    }
}

使用时,通过 delegate(on:) 完成订阅:

inputView.onConfirmInput.delegate(on: self) { (self, text) in
    self.textLabel.text = text
}

闭包的输入参数 (self, text) 和闭包 body self.textLabel.text 中的 self并不是原来的代表 controller 的 self,而是由 Delegateself 标为 weak 后的参数。因此,直接在闭包中使用这个遮蔽变量 self,也不会造成循环引用。

到这里为止的原始版本 Delegate 可以在这个 Gist 里找到,加上空行一共就 21 行代码。

问题和改进

上面的实现有三个小瑕疵,我们对它们进行一些分析和改进。

1. 更自然i的调用

现在,对 delegate 的调用时不如闭包变量那样自然,每次需要去使用 call(_:) 或者 call()。虽然不是什么大不了的事情,但是如果能直接使用类似 onConfirmInput(inputTextField.text) 的形式,会更简单。

Swift 5.2 中引入的 callAsFunction,它可以让我们直接以“调用实例”的方式 call 一个方法。使用起来很简单,只需要创建一个名称为 callAsFunction 的实例方法就可以了:

struct Adder {
    let value: Int
    func callAsFunction(_ input: Int) -> Int {
      return input + value
    }
}

let add2 = Adder(value: 2)
add2(1)
// 3

这个特性非常适合把 Delegate.call 简化,只需要加入对应的 callAsFunction 实现,并调用 block 就行了:

public class Delegate<Input, Output> {
    // ...
    
    func callAsFunction(_ input: Input) -> Output? {
        return block?(input)
    }
}

class TextInputView: UIView {
    @IBAction func confirmButtonPressed(_ sender: Any) {
        onConfirmInput(inputTextField.text)
    }
}

现在,onConfirmInput 的调用看起来就和一个闭包完全一样了。

类似于 callAsFunction 的直接在实例上调用方法的方式,在 Python 中有很多应用。在 Swift 语言中添加这个特性能让习惯于 Python 的开发者更容易地迁移到像是 Swift for TensorFlow 这样的项目。而这个提案的提出和审核相关人员,也基本是 Swift for TensorFlow 的成员。

2. 双层可选值

如果 Delegate<Input, Output> 中的 Output 是一个可选值的话,那么 call 之后的结果将会是双重可选的 Output??

let onReturnOptional = Delegate<Int, Int?>()
let value = onReturnOptional.call(1)
// value : Int??

这可以让我们区分出 block 没有被设置的情况和 Delegate 确实返回 nil 的情况:当 onReturnOptional.delegate(on:block:) 没有被调用过 (blocknil) 时,value 是简单的 nil。但如果 delegate 被设置了,但是闭包返回的是 nil 时,value 的值将为 .some(nil)。在实际使用上这很容易造成困惑,绝大多数情况下,我们希望把 .none.some(.none).some(.some(value)) 这样的返回值展平到单层 Optional.none.some(value)

要解决这个问题,可以对 Delegate 进行扩展,为那些 OutputOptional 情况提供重载的 call(_:) 实现。不过 Optional 是带有泛型参数的类型,所以我们没有办法写出像是
extension Delegate where Output == Optional 这样的条件扩展。一个“取巧”的方式是自定义一个新的 OptionalProtocol,让 extension 基于 where Output: OptionalProtocol 来做条件扩展:

public protocol OptionalProtocol {
    static var createNil: Self { get }
}

extension Optional : OptionalProtocol {
    public static var createNil: Optional<Wrapped> {
         return nil
    }
}

extension Delegate where Output: OptionalProtocol {
    public func call(_ input: Input) -> Output {
        if let result = block?(input) {
            return result
        } else {
            return .createNil
        }
    }
}

这样,即使 Output 为可选值,block?(input) 调用所得到的结果也可以经过 if let 解包,并返回单层的 result 或是 nil

3. 遮蔽失效

由于使用了遮蔽变量 self,在闭包中的 self 其实是这个遮蔽变量,而非原本的 self。这样要求我们比较小心,否则可能造成意外的循环引用。比如下面的例子:

inputView.onConfirmInput.delegate(on: self) { (_, text) in
    self.textLabel.text = text
}

上面的代码编译和使用都没有问题,但是由于我们把 (self, text) 换成了 (_, text),这导致闭包内部 self.textLabel.text 中的 self 直接参照了真正的 self,这是一个强引用,进而内存泄露。

这种错误和 [weak self] 声明一样,没有办法得到编译器的提示,所以也很难完全避免。也许一个可行方案是不要用 (self, text) 这样的隐式遮蔽,而是将参数名明确写成不一样的形式,比如 (weakSelf, text),然后在闭包中只使用 weakSelf。但这么做其实和 self 遮蔽差距不大,依然摆脱不了用“人为规定”来强制统一代码规则。当然,你也可以依靠使用 linter 和添加对应规则来提醒自己,但是这些方式也都不是非常理想。如果你有什么好的想法或者建议,十分欢迎交流和指教。

March 12, 2020 at 09:00AM via OneV’s Den https://ift.tt/2TGNAjC

VN:F [1.9.22_1171]
Rating: 0.0/5 (0 votes cast)
VN:F [1.9.22_1171]
Rating: 0 (from 0 votes)
Share

【酷壳】 与程序员相关的CPU缓存知识

好久没有写一些微观方面的文章了,今天写一篇关于CPU Cache相关的文章,这篇文章会讲述一些多核 CPU 的系统架构以及其原理,包括对程序性能上的影响,以及在进行并发编程的时候需要注意到的一些问题。这篇文章我会尽量地写简单和通俗易懂一些,主要是讲清楚相关的原理和问题,而对于一些细节和延伸阅读我会在文章最好给出相关的资源。

本文比较长,主要分成这么几个部分:基础知识、缓存的命中、缓存的一致性、相关的代码示例和延伸阅读。

因为无论你写什么样的代码都会交给CPU来执行,所以,如果你想写出性能比较高的代码,这篇文章中的技术还是应该认真学习的。

基础知识

首先,我们都知道现在的CPU多核技术,都会有几级缓存,老的CPU会有两级内存(L1和L2),新的CPU会有三级内存(L1,L2,L3 ),如下图所示:

其中:

  • L1缓分成两种,一种是指令缓存,一种是数据缓存。L2缓存和L3缓存不分指令和数据。
  • L1和L2缓存在第一个CPU核中,L3则是所有CPU核心共享的内存。
  • L1、L2、L3的越离CPU近就越小,速度也越快,越离CPU远,速度也越慢。

再往后面就是内存,内存的后面就是硬盘。我们来看一些他们的速度:

  • L1 的存取速度:
  • L2 的存取速度:
  • L3 的存取速度:
  • RAM内存的存取速度

我们可以看到,L1的速度是RAM的27倍,但是L1/L2的大小基本上也就是KB级别的,L3会是MB级别的。例如:Intel Core i7-8700K ,是一个6核的CPU,每核上的L1是64KB(数据和指令各32KB),L2 是 256K,L3有12MB(我的苹果电脑是 Intel Core i9-8950HK,和Core i7-8700K的Cache大小一样)。

于是我们的数据就从内存向上,先到L3,再到L2,再到L1,最后到寄存器进行CPU计算。为什么会设计成三层?这里有下面几个方面的考虑:

  • 一个方面是物理速度,如果你要更在的容量就需要更多的晶体管,除了芯片的体积会变大,更重要的是大量的晶体管会导致速度下降,因为访问速度和要访问的晶体管的位置成反比,也就是当信号路径变长时,通信速度会变慢。这部分是物理问题。
  • 另外一个问题是,多核技术中,数据的状态需要在多个CPU中进行同步,并且,我们可以看到,cache和RAM的速度差距太大,所以,多级不同尺寸的缓存有利于提高整体的性能。

这个世界永远是平衡的,一面变得有多光鲜,另一面也会变得有多黑暗。建立这么多级的缓存,一定就会引入其它的问题,这里有两个比较重要的问题,

  • 一个是比较简单的缓存的命中率的问题。
  • 另一个是比较复杂的缓存更新的一致性问题。

尤其是第二个问题,在多核技术下,这就很像分布式的系统了,要对多个地方进行更新。

缓存的命中

在说明这两个问题之前。我们需要要解一个术语 Cache Line。缓存基本上来说就是把后面的数据加载到离自己进的地方,对于CPU来说,它是不会一个字节一个字节的加载的,因为这非常没有效率,一般来说都是要一块一块的加载的,在CPU的缓存技术中,这个术语叫“Cache Line”(有的中文编译成“缓存线”),一般来说,一个主流的CPU的Cache Line 是 64 Bytes(也有的CPU用32Bytes和128Bytes),也就是16个32位的整型。也就是说,CPU从内存中捞数据上来的最小数据单位。

比如:Cache Line是最小单位(64Bytes),所以先把Cache分布多个Cache Line,比如:L1有32KB,那么,32KB/64B = 500 个 Cache Line。

一方面,缓存需要把内存里的数据放到放进来,英文叫 CPU Associativity。Cache的数据放置的策略决定了内存中的数据块会拷贝到CPU Cache中的哪个位置,因为Cache的大小远远小于内存,所以,需要有一种地址关联的算法,能够让内存中的数据可以被映射到cache中来。这个有点像内存管理的方法。

基本上来说,我们会有如下的一些方法。

  • 一种方法是,任何一个内存地址的数据可以被缓存在任何一个Cache Line里,这种方法是最灵活的,但是,如果我们要知道一个内存是否存在于Cache中,我们就需要进行O(n)复杂度的Cache遍历,这是很没有效率的。
  • 另一种方法,为了降低缓存搜索算法,我们需要使用像Hash Table这样的数据结构,最简单的hash table就是做“求模运算”,比如:我们的L1 Cache有500个Cache Line,那么,公式:`(内存地址 mod 500)x 64` 就可以直接找到所在的Cache地址的偏移了。但是,这样的方式需要我们的程序对内存地址的访问要非常地平均,这成了一种非常理想的情况了。
  • 为了避免上述的两种方案的问题,于是就要容忍一定的hash冲突,也就出现了 N-Way 关联。也就是把连续的N个Cache Line绑成一组,然后,先把找到相关的组,然后再在这个组内找到相关的Cache Line。

那么,Cache的命中率会成为程序运行性能非常关键的事,所以,了解上面的这些东西,会有利于我们知道在什么情况下有可以导致缓存的失效。

对于 N-Way 关联我们取个例子,并多说一些细节(因为后面会用到),Intel 大多数处理器的L1 Cache都是32KB,8-Way 组相联,Cache Line 是64 Bytes。于是,

  • 32KB的可以分成,32KB / 64 = 512 条 Cache Line。
  • 因为有8 Way,于是会每一Way 有 512 / 8 = 64 条 Cache Line。
  • 于是每一路就有 64 x 64 = 4096 Byts 的内存。

为了方便索引内存地址,

  • Tag:每条 Cache Line 前都会有一个独立分配的 24 bits来存的 tag,其就是内存地址的前24bits
  • Index:内存地址后续的6个bits则是在这一Way的是Cache Line 索引,2^6 = 64 刚好可以索引64条Cache Line
  • Offset:再往后的6bits用于表示在Cache Line 里的偏移量

如下图所示:(更多的细节可以读一下《Cache: a place for concealment and safekeeping》)

(图片来自《Cache: a place for concealment and safekeeping》)

这意味着:

  • L1 Cache 可映射 36bits 的内存地址,一共 2^36 = 64GB的内存
  • 因为只要头24bits相同就会被映射到同一个Way中,所以,每4096个地址会放在一Way中。
  • 当CPU要访问一个内存的时候,通过这个内存的前24bits 和中间的6bits可以直接定位相应的Cache Line。

此外,当有数据没有命中缓存的时候,CPU就会以最小为Cache Line的单元向内存更新数据。当然,CPU并不一定只是更新64Bytes,因为访问主存是在是太慢了,所以,一般都会多更新一些。好的CPU会有一些预测的技术,如果找到一种pattern的话,就会预先加载更多的内存,包括指令也可以预加载。这叫 Prefetching 技术 (参看,Wikipedia 的 Cache Prefetching纽约州立大学的 Memory Prefetching)。比如,你在for-loop访问一个连续的数组,你的步长是一个固定的数,内存就可以做到prefetching。(注:指令也是以预加载的方式执行,参看本站的《代码执行的效率》中的第三个示例)

缓存的一致性

对于主流的CPU来说,缓存的写操作基本上是两种策略(参看本站《缓存更新的套路》),

  • 一种是Write Back,写操作只要在cache上,然后再flush到内存上。
  • 一种是Write Through,写操作同时写到cache和内存上。

为了提高写的性能,一般来说,主流的CPU(如:Intel Core i7/i9)采用的是Write Back的策略,因为直接写内存实在是太慢了。

好了,现在问题来了,如果有一个数据 x 在 CPU 第0核的缓存上被更新了,那么其它CPU核上对于这个数据 x 的值也要被更新,这就是缓存一致性的问题。(当然,对于我们上层的程序我们不用关心CPU多个核的缓存是怎么同步的,这对上层都是透明的)

一般来说,在CPU硬件上,会有两种方法来解决这个问题。

  • Directory 协议。这种方法的典型实现是要设计一个集中式控制器,它是主存储器控制器的一部分。其中有一个目录存储在主存储器中,其中包含有关各种本地缓存内容的全局状态信息。当单个CPU Cache 发出读写请求时,这个集中式控制器会检查并发出必要的命令,以在主存和CPU Cache之间或在CPU Cache自身之间进行数据同步和传输。
  • Snoopy 协议。这种协议更像是一种数据通知的总线型的技术。CPU Cache通过这个协议可以识别其它Cache上的数据状态。如果有数据共享的话,可以通过广播机制将共享数据的状态通知给其它CPU Cache。这个协议要求每个CPU Cache 都可以“窥探”数据事件的通知并做出相应的反应。

因为Directory协议是一个中心式的,会有性能瓶颈,而且会增加整体设计的复杂度。而Snoopy协议更像是微服务+消息通讯,所以,现在基本都是使用Snoopy的总线的设计。

这里,我想多写一些细节,因为这种微观的东西,不自然就就会更分布式系统相关联,在分布式系统中我们一般用Paxos/Raft这样的分布式一致性的算法。而在CPU的微观世界里,则不必使用这样的算法,原因是因为CPU的多个核的硬件不必考虑网络会断会延迟的问题。所以,CPU的多核心缓存间的同步的核心就是要管理好数据的状态就好了。

这里介绍几个状态协议,先从最简单的开始,MESI协议,这个协议跟那个著名的足球运动员梅西没什么关系,其主要表示缓存数据有四个状态:Modified(已修改), Exclusive(独占的),Shared(共享的),Invalid(无效的)。

这些状态的状态机如下所示:

下面是个示例(如果你想看一下动画演示的话,这里有一个网页(MESI Interactive Animations),你可以进行交互操作,这个动画演示中使用的Write Through算法):

当前操作 CPU0 CPU1 Memory 说明
1) CPU0 read(x)  x=1 (E) x=1 只有一个CPU有 x 变量,
所以,状态是 Exclusive
2) CPU1 read(x)  x=1 (S) x=1(S) x=1 有两个CPU都读取 x 变量,
所以状态变成 Shared
3) CPU0 write(x,9)  x=9 (M) x=1(I) x=1 变量改变,在CPU0中状态
变成 Modified,在CPU1中
状态变成 Invalid
4) 变量 x 写回内存  x=9 (M) X=1(I) x=9 目前的状态不变
5) CPU1  read(x)  x=9 (S) x=9(S) x=9 变量同步到所有的Cache中,
状态回到Shared

 

MESI 这种协议在数据更新后,会标记其它共享的CPU缓存的数据拷贝为Invalid状态,然后当其它CPU再次read的时候,就会出现 cache misses 的问题,此时再从内存中更新数据。可见,从内存中更新数据意味着20倍速度的降低。我们能不直接从我隔壁的CPU缓存中更新?是的,这就可以增加很多速度了,但是状态控制也就变麻烦了。还需要多来一个状态:Owner(宿主),用于标记,我是更新数据的源。于是,现了 MOESI 协议

MOESI协议的状态机和演示我就不贴了,我们只需要理解MOESI协议允许 CPU Cache 间同步数据,于是也降低了对内存的操作,性能是非常大的提升,但是控制逻辑也非常复杂。

顺便说一下,与 MOESI 协议类似的一个协议是 MESIF,其中的 F 是 Forward,同样是把更新过的数据转发给别的 CPU Cache 但是,MOESI 中的 Owner 状态 和MESIF 中的 Forward 状态有一个非常大的不一样—— Owner状态下的数据是dirty的,还没有写回内存,Forward状态下的数据是clean的,可以丢弃而不用另行通知。

需要说明的是,AMD用MOESI,Intel用MESIF。所以,F 状态主要是针对 CPU L3 Cache 设计的(前面我们说过,L3是所有CPU核心共享的)。(相关的比较可以参看StackOverlow上这个问题的答案

程序性能

了解了我们上面的这些东西后,我们来看一下对于程序的影响。

示例一

首先,假设我们有一个64M长的数组,设想一下下面的两个循环:

const int LEN = 64*1024*1024;
int *arr = new int[LEN];

for (int i = 0; i < LEN; i += 2) arr[i] *= i;

for (int i = 0; i < LEN; i += 8) arr[i] *= i;

按我们的想法来看,第二个循环要比第一个循环少4倍的计算量,其应该也是要快4倍的。但实际跑下来并不是,在我的机器上,第一个循环需要127毫秒,第二个循环则需要121毫秒,相差无几。这里最主要的原因就是 Cache Line,因为CPU会以一个Cache Line 64Bytes最小时单位加载,也就是16个32bits的整型,所以,无论你步长是2还是8,都差不多。而后面的乘法其实是不耗CPU时间的。

示例二

我们再来看一个与缓存命中率有关的代码,我们以一定的步长increment 来访问一个连续的数组。

for (int i = 0; i < 10000000; i++) {
    for (int j = 0; j < size; j += increment) {
        memory[j] += j;
    }
}

我们测试一下,在下表中, 表头是步长,也就是每次跳多少个整数,而纵向是这个数组可以跳几次(你可以理解为要几条Cache Line),于是表中的任何一项代表了这个数组有多少,而且步长是多少。比如:横轴是 512,纵轴是4,意思是,这个数组有 4*512 = 2048 个长度,访问时按512步长访问,也就是访问其中的这几项:[0, 512, 1024, 1536] 这四项。

表中同的项是,是循环1000万次的时间,单位是“微秒”(除以1000后是毫秒)

| count |   1    |   16  |  512  | 1024  |
------------------------------------------
|     1 |  17539 | 16726 | 15143 | 14477 |
|     2 |  15420 | 14648 | 13552 | 13343 |
|     3 |  14716 | 14463 | 15086 | 17509 |
|     4 |  18976 | 18829 | 18961 | 21645 |
|     5 |  23693 | 23436 | 74349 | 29796 |
|     6 |  23264 | 23707 | 27005 | 44103 |
|     7 |  28574 | 28979 | 33169 | 58759 |
|     8 |  33155 | 34405 | 39339 | 65182 |
|     9 |  37088 | 37788 | 49863 |156745 |
|    10 |  41543 | 42103 | 58533 |215278 |
|    11 |  47638 | 50329 | 66620 |335603 |
|    12 |  49759 | 51228 | 75087 |305075 |
|    13 |  53938 | 53924 | 77790 |366879 |
|    14 |  58422 | 59565 | 90501 |466368 |
|    15 |  62161 | 64129 | 90814 |525780 |
|    16 |  67061 | 66663 | 98734 |440558 |
|    17 |  71132 | 69753 |171203 |506631 |
|    18 |  74102 | 73130 |293947 |550920 |

我们可以看到,从[9,1024] 以后,时间显注上升。包括[17,512] 和 [18,512] 也显注上升。这是因为,我机器的 L1 Cache 是 32KB, 8 Way 的,前面说过,8 Way的一个组有64个Cache Line,也就是4096个字节,而1024个整型正好是 4096 Bytes,所以,一旦过了这个界,每个步长都无法命中 L1 Cache,每次都是 Cache Miss,所以,导致访问时间一下子就上升了。而 [16, 512]也是一样的,其中的几步开始导致L1 Cache开始失效。

示例三

接下来,我们再来看个示例。下面是一个二维数组的两种遍历方式,一个逐行遍历,一个是逐列遍历,这两种方式在理论上来说,寻址和计算量都是一样的,执行时间应该也是一样的。

const int row = 1024;
const int col = 512
int matrix[row][col];

//逐行遍历
int sum_row=0;
for(int r=0; r<row; r++) {
    for(int c=0; c<col; c++){
        sum_row += matrix[r];
    }
}

//逐列遍历
int sum_col=0;
for(int c=0; c<col; c++) {
    for(int r=0; r<row; r++){
        sum_col += matrix[r];
    }
}

然而,并不是,在我的机器上,得到下面的结果。

  • 逐行遍历:0.081ms
  • 逐列遍历:1.069ms

执行时间有十几倍的差距。其中的原因,就是逐列遍历对于CPU Cache 的运作方式并不友好,所以,付出巨大的代价。

示例四

接下来,我们来看一下多核下的性能问题,参看如下的代码。两个线程在操作一个数组的两个不同的元素(无需加锁),线程循环1000万次,做加法操作。在下面的代码中,我高亮了一行,就是p2指针,要么是p[1],或是 p[18],理论上来说,无论访问哪两个数组元素,都应该是一样的执行时间。

void fn (int* data) {
    for(int i = 0; i < 10*1024*1024; ++i)
        *data += rand();
}

int p[32];

int *p1 = &p[0];
int *p2 = &p[1]; // int *p2 = &p[30];

thread t1(fn, p1);
thread t2(fn, p2);

然而,并不是,在我的机器上执行下来的结果是:

  • 对于 p[0]p[1] :560ms
  • 对于 p[0]p[30]:104ms

这是因为 p[0]p[1] 在同一条 Cache Line 上,而 p[0]p[30] 则不可能在同一条Cache Line 上 ,CPU的缓冲最小的更新单位是Cache Line,所以,这导致虽然两个线程在写不同的数据,但是因为这两个数据在同一条Cache Line上,就会导致缓存需要不断进在两个CPU的L1/L2中进行同步,从而导致了5倍的时间差异

示例五

接下来,我们再来看一下另外一段代码:我们想统计一下一个数组中的奇数个数,但是这个数组太大了,我们希望可以用多线程来完成,这个统计。下面的代码中,我们为每一个线程传入一个 id ,然后通过这个 id 来完成对应数组段的统计任务。这样可以加快整个处理速度。

int total_size = 16 * 1024 * 1024; //数组长度
int* test_data = new test_data[total_size]; //数组
int nthread = 6; //线程数(因为我的机器是6核的)
int result[nthread]; //收集结果的数组

void thread_func (int id) {
    result[id] = 0;
    int chunk_size = total_size / nthread + 1;
    int start = id * chunk_size;
    int end = min(start + chunk_size, total_size);

    for ( int i = start; i < end; ++i ) {
        if (test_data[i] % 2 != 0 ) ++result[id];
    }
}

然而,在执行过程中,你会发现,6个线程居然跑不过1个线程。因为根据上面的例子你知道 result[] 这个数组中的数据在一个Cache Line中,所以,所有的线程都会对这个 Cache Line 进行写操作,导致所有的线程都在不断地重新同步 result[] 所在的 Cache Line,所以,导致 6 个线程还跑不过一个线程的结果。这叫 False Sharing。

优化也很简单,使用一个线程内的变量。

void thread_func (int id) {
    result[id] = 0; 
    int chunk_size = total_size / nthread + 1;
    int start = id * chunk_size;
    int end = min(start + chunk_size, total_size);

    int c = 0; //使用临时变量,没有cache line的同步了
    for ( int i = start; i < end; ++i ) {
        if (test_data[i] % 2 != 0 ) ++c;
    }
    result[id] = c;
}

我们把两个程序分别在 1 到 32 个线程上跑一下,得出的结果画一张图如下所示:

上图中,我们可以看到,灰色的曲线就是第一种方法,橙色的就是第二种(用局部变量的)方法。当只有一个线程的时候,两个方法相当,而且第二种方法还略差一点,但是在线程数增加的时候的时候,你会发现,第二种方法的性能提高的非常快。直到到达6个线程的时候,开始变得稳定(前面说过,我的CPU是6核的)。而第一种方法无论加多少线程也没有办法超过第二种方法。因为第一种方法不是CPU Cache 友好的。

篇幅问题,示例就写到这里,相关的代码参看我的Github相关仓库

延伸阅读

(全文完)


关注CoolShell微信公众账号和微信小程序

(转载本站文章请注明作者和出处 酷 壳 – CoolShell ,请勿用于任何商业用途)

——=== 访问 酷壳404页面 寻找遗失儿童。 ===——

陈皓 March 02, 2020 at 03:43AM
from 酷 壳 – CoolShell https://ift.tt/2VAUecw

VN:F [1.9.22_1171]
Rating: 0.0/5 (0 votes cast)
VN:F [1.9.22_1171]
Rating: 0 (from 0 votes)
Share

赏梅时节

周六一早出门,趁着人潮来袭之前,去了一趟梅花山。在南京那么多年,都没有去过一次。

全程戴口罩,进园的时候人不多,11点多出来的时候已经不少人了,赶紧回家。

VN:F [1.9.22_1171]
Rating: 0.0/5 (0 votes cast)
VN:F [1.9.22_1171]
Rating: 0 (from 0 votes)
Share

维基百科2020年2月15日典范条目

1879S Morgan Dollar NGC MS67plus Reverse.png

摩根銀元美國鑄幣局於1878至1904年間生產的一種1美元硬幣,之後還在1921年再度投產。國會通過《1873年鑄幣法案》為自由鑄造銀幣運動劃上句點,坐姿自由女神銀元因此停產,摩根銀元則是此後美國發行的第一種標準銀元。硬幣由鑄幣局助理雕刻師喬治·T·摩根設計並以他命名,其正面刻有自由女神肖像,背面則刻有展開翅膀的老鷹,爪子上還抓有箭和橄欖枝。國會於1876年秋通過布蘭德-阿利森法,授權發行新銀元,並要求財政部每個月按市場價購買200至400萬美元白銀並打造成銀幣。1890年,國會又通過謝爾曼收購白銀法,其中把財政部每個月要購買的白銀量大幅提升到14萬公斤,但在銀元生產上則只要求鑄幣局再繼續一年。這一法案之後在1893年廢除。1898年,國會通過新法案,要求之前根據謝爾曼收購白銀法買下的所有銀錠都要打造成銀元,鑄幣局於是繼續生產到1904年才把庫存的銀錠用完,摩根銀元也暫時停產。1918年通過的皮特曼法案授權將數以百萬計的銀元熔解後再重新鑄造,摩根銀元於是又在1921年再次投產,同年末,新設計的和平銀元面世,摩根銀元的設計正式退出歷史舞台。

February 15, 2020 at 08:00AM
from 维基百科典范条目供稿 https://ift.tt/2vx1vza

VN:F [1.9.22_1171]
Rating: 0.0/5 (0 votes cast)
VN:F [1.9.22_1171]
Rating: 0 (from 0 votes)
Share

维基百科2020年2月14日典范条目

Tropical Storm Edouard 2002.jpg

2002年热带风暴爱德华2002年大西洋飓风季的第五个热带风暴。爱德华于9月1日从佛罗里达州以东的一片对流区和冷锋发展成热带气旋,在弱转向气流的影响下,系统向北漂移并沿顺时针路径循环向西移动。虽然有中等到较强风切变的不利影响,风暴还是在9月3日达到风速每小时约100公里的最高强度,但又在接下来向西行进的过程中快速减弱。9月5日,爱德华在佛罗里达州东北部实现登陆,并在穿越该州后于9月6日消散,其残留被热带风暴费伊的环流吸收。热带风暴爱德华给佛罗里达州带去了中等程度降水,其中该州西部降雨量超过175毫米。虽然登陆时系统仍有热带风暴强度,但其在陆地上行进时的风速很小。此外虽有多条道路被雨水淹没,但此次风暴没有造成人员伤亡,总体破坏也很小。

February 14, 2020 at 08:00AM
from 维基百科典范条目供稿 https://ift.tt/2vxnaXQ

VN:F [1.9.22_1171]
Rating: 0.0/5 (0 votes cast)
VN:F [1.9.22_1171]
Rating: 0 (from 0 votes)
Share

维基百科2020年2月13日典范条目

Florida counties map.png

美國佛羅里達州一共有67個,於1821年成為美國領土時只有兩個郡,即以薩瓦尼河(Suwanee River)為界,西部為艾斯康比亞郡,東部為聖約翰斯郡,後來成立的其它所有郡都曾是這兩個郡的一部分。佛羅里達於1845年成為美國的第27個,1925年由阿拉楚阿郡析置的吉爾克里斯特郡是全州最年輕的郡。佛羅里達州的郡是州政府的分支。1968年,各郡獲得了自行發放特許狀的權力。除了瓦古拉郡郡城克勞福德維爾外,佛羅里達州的其他所有郡城都是註冊成立的自治市

February 13, 2020 at 08:00AM
from 维基百科典范条目供稿 https://ift.tt/2OLug1O

VN:F [1.9.22_1171]
Rating: 0.0/5 (0 votes cast)
VN:F [1.9.22_1171]
Rating: 0 (from 0 votes)
Share

【四火】 技术面试中,什么样的问题才是好问题?

其实很久以前就想谈一谈这个话题了,但是最近才有了足够的动机。因为从最近参加的很多 debrief 来看,我认为身边大多数的软件工程师面试中,在通过技术问题来考察候选人这方面,很多都做得不够好。比方说,我看到对于一些经验丰富的软件工程师候选人的面试,一些面试官依然是草率地扔出一道算法题让做了事,并且认为能不能够比较清晰完整地将代码写出来,是工程师级别裁定的最重要的标准。而这样的做法我认为是非常不妥的。

首先,我要明确的是,这个问题,指的是技术面试中俗称的 “主要问题”,具体来说,就是面试官会拿出一个问题和候选人讨论,并通过由此开始双方的互相沟通和问题发散来达到考察的目的,因此,这个 “问题”,从某种角度说,更像一个 “话题”。这个过程通常在每轮面试中会持续几十分钟(如果你对这种面试方式感兴趣,你可以看一下这个简单的介绍)。下面的讨论,都是建立在这种面试风格和方式之上的。

其次,作为一个 disclaimer,我想说,以下内容来自于我的认识,并且是针对于技术面试这一个狭窄范围内的认识,自然带有主观的倾向性和认知的局限性,它并不是来自任何公司或组织的标准。

好,下面我就来尝试把这个问题讲清楚、讲透彻。我认为这并不是一件容易的事情,因此如果你对其有不同的看法,欢迎和我一起讨论。

典型案例

我先来举这样一个典型的例子,这里面包含了若干个值得商榷的方面,你可以看看是不是似曾相识:

在和候选人谈论完项目和经历以后,面试还有 40 分钟,于是面试官问:你能否实现一个 LRU 队列?

于是候选人想了一下,就开始做题了,也就是在白板上大写特写,于是面试官也就开始忙自己的事儿了。等到 40 分钟后,候选人写了一白板,但是显然,他在这过程中遇到了一些困难,最后虽然实现了,但是代码写得有些复杂,也遗漏了两、三个重要的 corner case。

于是面试之后,面试官在评语中写道,“候选人能力一般,算法题实现起来磕磕绊绊,最后的代码偏臃肿,而且有明显的 bug”。

在往下阅读以前,请你想一想,这样的面试形式有哪些值得商榷的地方?

技术面试的目的

好,我先卖个关子,先不回答上面的问题,而是先谈一谈,对于软件工程师候选人来说,我们为什么要进行这样的技术面试。事实上,有很多考察项,完全不需要技术面试这样麻烦的途径,就可以很容易、很高效地实现;而下面我说的这些方面,这些对于软件工程师来说至关重要的方面,技术面试却是合理考察的唯一可行途径。

技术能力方面

通常不同轮次的面试,会考察不同的技术点,这个对于不同的团队和职位,是不一样的。

举例来说,对于某业务平台团队的一个高级工程师的职位,五轮面试中,一轮考察项目和经验,一轮考察系统设计,两轮考察具体问题的解决,特别包括算法和数据结构,还有一轮考察面向对象设计等其它内容,这其中,后三轮都包括白板编码的考察。

对于一个有经验的工程师候选人来说,我认为这就是一个比较立体、综合,也是一种比较合理的技术考察点的划分方式。并且,这五轮中有四轮都会花费大量的时间,通过我今天将谈到的技术问题,来对候选人进行技术能力方面的评估。这里,有这样几个非常常见的技术方面的考察项:

  1. 分析问题,整理需求的能力。问题在一开始可能很模糊,但是优秀的且有经验的工程师可以识别出核心的诉求来,这个 “识别” 的能力,下文我还会详述。这里的诉求可能有多个,但是考虑到时间的关系,面试过程中往往只会从某个角度覆盖其中一两个。
  2. 根据需求来设计系统的能力。这里既包括功能性需求,又包括非功能性需求,前者是必须要涉及到的,但是后者也经常也放在一起考量。其实这一点可大可小,它未必一定指系统设计中,功能是否得到实现,并且这个过程中,可能会涉及到系统的扩展性、可用性、一致性等等方面。
  3. 将核心逻辑实现的能力。如今,考察比重容易被高估的算法和数据结构就大体上属于这一部分。它也有功能和非功能的两个角度——功能上算法能否实现需求,非功能上算法是否具备足够的性能,编码是否遵循最佳实践,代码是否具备良好的可扩展性等等。而编码能力,指的是在思路达成一致以后,核心逻辑能不能落到纸面(代码)上。毕竟,“空谈误国,实战兴邦”。
  4. 经验和其它工程能力。这部分相对更为灵活。比如对测试能力的考察,即可以做怎样的测试来实现对于功能需求和非功能需求正确性的保证。对于特定的团队和项目来说,有时候会特别专注于特定的技术能力,比如前端的团队,是需要考察前端的基础技能的。

考虑到时间和覆盖面、覆盖深度的权衡,上面这四点有时候不能在一轮面试中完全覆盖,往往也会包括三点。比如,系统设计的面试可以着重覆盖 1、2、4 点,而编码为主的面试可以着重覆盖 1、3、4 点。

非技术能力方面

和技术能力考察所不同的是,非技术能力考察在不同面试官中的特异性更大,换言之,每个人的角度和标准都可能不相同。但我觉得下面这几条是特别重要,因而必须要覆盖到的:

  1. 沟通合作的能力。如果不计时间成本,最理想的面试是什么?其实就是一起工作。工作中才会有足够多必要的沟通,无论是正面的品质还是负面的问题都会无所遁形。可是面试的时间有限,我们没有办法实现真正的工作氛围,但依然可以模拟工作中一起考察、分析和解决问题的过程,而这个过程,就是要通过 “沟通” 串起来的。沟通是一个大的话题,具体的考察项有很多,比如,能不能接受建议?能不能讨论想法?有些候选人一旦进入自己的思考模式就听不进别的话了,于是一个点要反复强调几遍;而有的则是缺少 backbone,稍微追问一下,也不思考,就立马改变主意。
  2. 热情和兴趣。热情和兴趣的影响是巨大的,都说兴趣是最好的老师,这部分是很难 “教出来” 的。对于初级工程师来说更是如此。热情和兴趣不但会影响到他/她自己的未来发展,也会影响到整个团队的氛围。
  3. 学习能力。学习能力很大程度决定了候选人在新的岗位上进步的潜力。同样的基础和基本的问题解决能力,有的候选人能够 “一点就透”,触类旁通,这在一定程度上就反映了学习的能力。毫无疑问,软件工程师每天都在面对新问题,入职以后就会发现,不止问题是新的,代码是新的,类库是新的,工具是新的,在成熟到能够有一定产出之前,这一步一定是学习。

技术能力和非技术能力,哪个更占主导?事实上,这二者都很重要,并且二者各自又具备不同的特点。当然相对来说,非技术能力更加难以提高,因而这方面的问题要更加引起重视。比方说,要让一个对于软件领域缺乏热情和兴趣的候选人,在入职以后改头换面,是几乎不可能的一件事情。

其它方面

上面说的是技术面试中对于 “能力” 的考察。其实,还有一些考察项,严格来说并不能算是 “能力”,因此我就没有归类在上面,也不是本文的重点,但这并不是说它们不重要。

比如,候选人是否具备正直的品格。在 OCI,debrief 的结果,通常有 hire 和 no hire 两种,但是有一种情况,可以归结到一个特殊的 “never hire” 里面去,这样的候选人没有面试冷冻期,不会有职位和级别的讨论,就是一个 “永不考虑” 聘用——这就是品格问题。品格问题会导致 never hire 的出现,比如候选人对当前的所在职位说谎了。

再比如,和团队的契合程度。不同的团队,接纳候选人的程度和要求都是不一样的。一个典型的例子是,有时候我们发现,有的候选人在投票的边界线上,即本身是具备相当的潜力的,但是由于相对缺乏领域经验,且某些方面显示出方法明显不得要领。如果团队中有成熟、有经验的工程师可以带着,且团队有一定的空间允许他花更长的时间学习和成长,那么最后的结论就是 hire,否则就是 no hire。你也可以看出,很多时候这样的决定都不是非黑即白的,影响的因素是多方面的。

再再比如,性格不兼容导致的风险。我遇到过一例,候选人在面试过程中,在多轮面试中都表现出高傲和自满的个性来。于是这成为了一个担心招聘进来以后,风险过高的重要方面。于是最后我们放弃了这个候选人,尽管这个候选人的技术方面是没有问题的。有人可能会说,聪明人都是有个性的。但其实 “有个性” 和 “难相处” 却有着微妙的差别,而且再包容的团队,也有自己的底线。我们当然不希望错过优秀的人才,但是这并不是不计代价的。

从这几个方面也能看出,这些 “非能力” 的考察项,往往具备着或 0 或 1 的 “red flag” 的特点。面试官一般不会花心思在这部分的考察上,但如果发现这方面的问题,且经过了明确。那结果往往就会是一个明确的否决票,而这个否决票是和级别、职位无关的。

回看那个案例

讲完了技术面试的目的,再来回看那个案例。那个案例中,所记叙的面试过程,对于技术面试的技术能力和非技术能力的考察,是否有覆盖呢?我们不妨一条一条看吧:

技术能力方面:

  1. 分析问题,整理需求的能力。这一条考察的程度明显是不够的,给出的问题,是一个明确的、具体的算法题,也就是要实现一个 LRU 队列。也许这其中存在着问题分析和需求整理的空间,但对于具体算法题来说,这个空间显然并不大,而且候选人闷头就写了,这方面无从考察。对于一个初级工程师的面试来说可能还好一些,我通常没有微词;可对于一个要去面试一个经验丰富的工程师,我是很不赞成纯算法题面试的,而这就是其中的一个重要原因。
  2. 根据需求来设计系统的能力。这一点的覆盖基本为 0。上来就写代码了,不清楚思考的过程,也更谈不上什么系统设计了。
  3. 将核心逻辑实现的能力。这条确实是这种面试方式能够覆盖的部分,因为整个过程,就是候选人思考并编码实现的过程,只不过,面试官能得到的只有一个 “结果”,而非整个 “过程”。这样的数据,能反映出来在核心逻辑实现方面的价值,就要大打折扣了。
  4. 经验和其它工程能力。也许能够从代码的实现上获知一部分,但这一条考察的程度也显然是很不够的。

再来看非技术能力方面:

  1. 沟通合作的能力。这是最大的问题,因为这方面是远远不足的,整个过程没有沟通,没有合作,只有默默地做题。
  2. 热情和兴趣。这个过程很难从这个角度获取足够的候选人在热情和兴趣方面展示出来的信息。
  3. 学习能力。同上。

也就是说,除了 “将核心逻辑实现的能力” 可能还勉强过得去,这样的面试方式,并无法全面、合理地考察候选人作为软件工程师的综合素质。

事实上,如果你联想实际工作。如果你的团队中有这样一个工程师,拿到一句简单的需求,不确认问题,不沟通设计,不讨论方案,直接就开始埋头苦干,就算能写出可以工作的代码来,这是不是依然是一件无比恐怖的事情?显然,我们的面试要尽可能避免这样的事情在真实世界中发生。我们要找的是软件工程师,不是只会刷题编码者。

这也是我把这个案例,放在开头,作为反面案例的原因之一。

等等,“之一”!难道还有别的原因?

是的,这还没完,这个案例还有着其它弊端,我想再卖个关子——而现在,你可以想一想,再往下看。

怎样的问题才是 “好” 问题?

终于要正面回答标题中的问题了,到底怎样的问题才能真正称得上 “好” 问题呢?下面是我认为最重要的几条衡量标准。

从模糊到清晰

首先,这个问题在一开始要足够模糊,以便让候选人可以逐层递进,逐步细化,寻根究底。这个过程,其实就是将 “具体问题” 经过分析、归纳、思考、抽象并将其映射成为一个 “软件问题” 的过程。在问题变得清晰的过程中,理想的情况是,候选人可以表现出主动性,即候选人可以在多数情况下引领讨论的思路,而不是面试官。面试官需要顺着候选人的思路,逐步框定下问题的讨论范畴,并明确到其核心实现是确实可以用软件的办法实现的。

在这样的状态下,候选人可以以自然的状态,具备相当自由度地发挥自己的能力。从这个过程中,可以观察得到太多候选人的不同角度的特质了。通过这种方式,也可以很大程度避免了已经知道 “标准答案” 的面试官,由于思路的局限性,而给面试施加的源自于主观偏好的影响。

这就好像是开放世界的 RPG 游戏,有多个不同的路径都可以完成任务,玩家可以决策并决定主角的走向,但是这一切始终还要在游戏设计者的掌控之内。这当然是说的理想状态,有时候会有偏差,但我们朝着这个方向努力。这也对面试官驾驭不同的状况有着很高的要求,毕竟,面试官要对这个问题前前后后足够的熟悉,以便应对各种不同的细化场景。有一个常见的方式,是可以从一个自己已经足够熟悉的问题开始,比如自己曾经多年工作涉及的某类系统。

我来举一个具体例子。比如,有这样一个问题:

怎样设计一个流量控制系统?

这就是一个模糊到没法再模糊的问题了。不知你会不会产生下面这样的问题:

  • 什么系统需要流量控制?
  • 现在的流量是多少?
  • 需要支持到什么时间精度?
  • 流量控制的规则怎么定义?
  • 超过流量的请求怎么处理?
  • ……

其实,这些都或多或少是需要面试官和候选人一起逐步思考、分析和明确的。在这个过程中,可以考察的内容太多太多了。

事实上,针对不同程度的候选人,上述这个问题给出的最原始的模糊程度是不一样的,问题越是模糊,这部分对于候选人的要求也就越高。对于一个工作十多年的,有着多年系统设计经验的工程师来说,上面这些问题大致都应该是他/她能够主动提问,或是主动引领明确的。

值得一提的是,理想的问题最好还有一些隐藏的 “坑”,能否把这些坑识别出来,也是对于工程能力方面,一个很好的小的考察点。比方说,优秀的候选人应该想到,流量控制可以基于绝对时间窗口,或是相对时间窗口来进行的,但是要真正保护系统,相对时间窗口才是最理想的。当然在实现难度上,相对时间窗口,往往会更难一些。

而对于一个没有工作经验,并将要研究生毕业的候选人来说,问这样一个模糊的问题,往往带来了过大的难度,不但不容易推进面试的进程,还可能给候选人带来沮丧的心理。我们不希望看到,候选人拿到问题以后就懵了,如果发现候选人推进有困难,面试官需要介入并帮助。

因此根据候选人的程度,这需要面试官主动回答这些问题,或是直接缩小或明确问题的范畴,当这个问题的范畴缩到最小时,这可以是一个直接存在多种解法的算法题。极端地说,这个问题可以一直缩小到这样的程度:

假定说有这样一个 API,名字叫做 isAllowed,这个 API 在系统每次收到请求的时候就调用,传入的是请求对象,传出 boolean 值表示是否允许这次调用——如果最近一分钟内调用次数小于 10000 次就允许,反之则不允许。你能否将这个 API 实现出来?

如果候选人还一脸迷茫,可以提供这样的参考 API:

class RateLimiter {
    public boolean isAllowed(Request req) {
        // TODO
    }
}

你看,这只是一个将问题明确、细化和分解的过程,并没有涉及到实际实现代码该用的算法。但是,上面提的那些问题,要么都通过这个例子明确了,要么都给出具体数字了,这本身,就将一个模糊的问题,降低难度明确为一个具体的算法问题了。

不止一个解

前面一步已经谈到了有不同的方式可将模糊的 “实际问题” 映射到了一个可解的 “软件问题”,那么现在,这个 “软件问题” 依然没有标准答案。可能有几个参考答案,它们互相比较起来各有优劣。大多数情况下,候选人的思路,都在这几个参考答案的思路之中,但有时也能看到特立独行的新奇思路。

如同前一步所说的那样,对于不同级别的软件工程师职位来说,需求分析、系统设计等等这些方面的要求可能有着很大的差别;但是在这一步,对于数据结构和算法这样的基础能力,却是接近的。

对于这里谈到的流量控的算法来说,实现方式是有很多种的,代码复杂程度,控制精度,时间复杂度和空间复杂度等等都有着非常大的区别。当然此时涉及到的,已经基本只是算法层面的话题了,就算法本身而言,我在极客时间专栏中对其中的几个典型方法做了一些介绍,感兴趣的话可以阅读。

我可以再举一个我曾经经常在面试中拿来使用的例子:

某社交网站有两百万的注册用户,每个用户都有积分属性,且积分根据用户在社交网站上的行为而不断有小幅度的频繁变更(比如登陆一次就+1 分,评论一次就+2 分等等),怎样设计一种算法,能够高效、准确地实时获取指定用户在所有用户中基于积分的排名?

上面的问题从模糊逐步落实到实现上的时候,异步、定时地排序,是最容易想到的方案,而题目表述中的 “高效” 和 “实时” 这两个修饰词让这个问题变得困难。这个过程中,我见到的不错的办法就至少有七、八种,比方说,下面这个推进问题解决的例子:

  1. 候选人:在需要的时候进行排序,方案是……
  2. 面试官:好,这样的方式下,时间、空间复杂度是多少?
  3. 候选人:……(说着说着自己意识到时间消耗可能巨大)
  4. 面试官:对,不仅时间、空间都消耗巨大,CPU 也是,你能否优化?
  5. 候选人:……(提出了一些优化思路,但是他自己对它们的实时性也不满意)
  6. 面试官:好,有换个角度更进一步优化的方式吗?
  7. 候选人:对了,可以让数据一直是排好序的!
  8. 面试官:好主意,那你怎么设计数据结构呢?
  9. 候选人:我可以使用一个 map 来保存用户 id 到积分的映射,再把积分从小到大按序放在数组中,这样二分查找就可以找到对应积分所处的排名。
  10. 面试官:听起来不错,那么这时候获取排名的复杂度是多少?
  11. 候选人:……
  12. 面试官:对,每当用户的积分小幅变化的时候,你怎么维持这个数组依然有序?
  13. 候选人:从数组中拿掉一个老的积分,再放入一个新的积分……
  14. 面试官:这个变更影响的数据量有多少,时间复杂度又是如何?
  15. 候选人:……
  16. 面试官:不错,可这个方法有什么问题吗?
  17. 候选人:(恍然大悟)如果新添加一个用户,新的积分会出现在数组头部,数组内的所有数据都要向后移动一个单位!
  18. 面试官:没错,那你打算怎么优化?
  19. 候选人:可以把数组内的积分从大到小排序,这样新添加的用户所对应的积分总在尾部。
  20. 面试官:很好,这个方法还有什么问题吗?
  21. 候选人:……(意识到在某些情况下,有很多用户拥有相同的积分,这时时间复杂度会退化)
  22. 面试官:那样的话,你怎么优化?
  23. 候选人:数组的元素除了记录当前积分,还记录有多少个用户具有这个积分,从而消除相同积分的重复元素……
  24. 面试官:很好,可这个方法会带来一个问题,你能想到吗?
  25. 候选人:对了,如果积分变化以后,新的积分是没有出现过的,那么添加到数组里,就是一个新元素,于是所有比它小的积分全部都要向后移动一个单位。
  26. 面试官:非常好,那么你怎么优化?
  27. 候选人:如果使用链表来代替数组就可以避免这个问题,(突然意识到)可是链表我就没法二分查找了……
  28. 面试官:没错,那什么样的数据结构和链表在这方面具有一定相似性,又能够具备二分查找相似的时间复杂度?
  29. 候选人:……(这一步能回答出来答案就很多了,很多都是很不错的思路,比如有用跳表的,有用二叉搜索树的等等)

这只是一个简化了的片段,实际的沟通的内容远比这部分内容多,但是从中也依然可以管中窥豹,看出问题解决的过程是怎样逐步推进的。

从这里也可以看出,无论是从实际问题细化到软件问题,还是求解这个软件问题,都存在着多条通往罗马的道路,看起来很美好,但这样的问题设计和面试把控并不容易。但既然大家都是软件工程师,是未来有可能一起工作的工程师,面试官的能力和可能就和候选人接近,于是,为了保证面试的效果,就一定要精心准备这样的问题,而不能指望随机和临场想出来一个 “好” 的问题提问。

围绕问题的解决要完整

这个问题的分析、讨论和解答过程要完整。对,其实这一点说的已经不是问题本身了,而是攻克这个问题的过程了。

这指的是整个过程要努力让候选人能够抵达 “踮踮脚能够到” 的难度,并且能够完成从确认、分析、讨论、编码、验证和改进等一个过程。这让整个面试显得完整,同时带来了这样两大好处:

  • 对于面试官来讲,这样一个完整的过程,可以更全面地考察候选人,避免陷入视角过窄和一叶障目的情境。同时,“踮踮脚能够到” 的难度,又可以给整个考察的进程具备较为合理的预期。
  • 对于候选人来讲,心态可以得到一定程度的平复,不沮丧,能够 “完整地” 面试完一轮,能够收获信心。别忘了,面试是双向的,给候选人一个良好的印象是很重要的。

前文我举的这个将问题从模糊到逐步清晰化的这个例子,就是一个需要面试官根据候选人情况动态调整的例子。在候选人能够经过思考而快速推进问题解决进展的时候,要让出主动权,以被动回答和鼓励为主;但在候选人卡壳的时候,要夺回主动权,及时给出提示和引导。

在落到数据结构和算法上面的时候,极少有候选人能够在叙述思路的时候直接给出最优解的。这时候,如果时间充裕,特别是在候选人进展非常顺利的时候,可以不断提示、追问以要求 “代码前优化”,一步一步优化到他/她的问题解决的能力边界,这就是其中的一个探寻其 “踮踮脚能够到” 的这个问题的解决能力的一个办法。但这个过程的前提,是一定要给编码留足时间。当然,如果候选人不能在限定时间内给出清晰的优化后思路,那不妨就退一步到原先那个算法角度不那么 “好”,但是思路清晰的解法上,并落实到代码。

比方说,对于前文所述的那个流量控制的问题,候选人在还有半个小时的时候就想到了使用一个时间复杂度为 O(N) 的解,而面试官认为时间还比较充裕,那就可以尝试挑战一下候选人 “能否再优化一下复杂度?”。

有些时候,由于前面的过程磕磕绊绊,时间剩下不多了,依然只有时间复杂度较高的 brute force 的解法,那么将这个解法实现了,其实也是一个不错的选择。有时候时间实在比较紧张,可以要求实现一部分核心代码,这些都比由于时间太短而代码写了一半匆匆收尾要好得多。我的经验是,在讨论充分,思路清晰的情况下,代码完成的时间,一般只需要 10 到 15 分钟,这样的代码量对于面试来说是比较合理的。

当然,相较而言,一种更为糟糕的结果是,一直到最后,讨论的深度依然离编码尚远,甚至依然停留在一个很高的泛泛而谈的层面。

如果在编码完成之后,尚有时间,优秀的候选人会拿实际的例子去验证代码的正确性。而面试官也可以和候选人讨论 “代码后优化”,比如以下的问题:

  • 你能否进一步优化算法以提高时间/空间复杂度?
  • 如果是工业级别的代码,你觉得代码还有哪些问题?
  • 你该怎样去设计测试,来保证这段代码的正确性 ?

对考察项的覆盖兼有深度和广度

这个问题要能够考察前文所述的技术能力和非技术能力。

这里说的覆盖,不一定要全部覆盖,但是要覆盖其中的大部分,并且对于每一个考察项要具备一定的考察深度。我见到过不少其它的面试风格,但我认为这样就着一个模糊的主要问题(话题)逐步展开的方式是最好的。因为它可以兼具广度和深度的平衡。具体来说:

面试很容易走向的一个极端就是考虑广度,但缺乏深度。比如一种风格是绝大部分的面试时间用来询问候选人的项目和经验,让候选人自己介绍,而面试官跟进追问。这原本是一种很好的方式,但是由于候选人对自己的项目通常远比面试官熟悉得多,除非明确的同一领域,否则面试官较难对于其中的内容挖掘到足够的深度,从而识别出候选人是真正做事的人,还是夸夸其谈的人。这也是这种方式理论可行,但实际开展难度较大的原因之一。

另一个极端,自然就是考虑深度,但缺乏广度。比如给出一个过于具体的问题,缺乏发挥和迂回的空间,对这个问题所涉及的很小一部分深度挖掘,甚至纠缠于某个特殊而单一的 case 很长时间,但是却只能覆盖很少的考察角度。

由于深度和广度都是可控的,那么这样的可以拿来问不同经验和不同技术背景的工程师候选人。这样对于面试官来说,可以获得足够的数据,便于在遇到新的候选人的时候,能够进行横向比较,做出更准确的评估。比方说,过去某级别的候选人能够在面对这个问题的时候,能够达到什么样的级别,而如今这个候选人有着类似表现,这就可以以过去的那个例子来作为参考比较了。

“不好” 的问题

现在,让我们再次回到文章开头那个例子问题,除了已经提到的考察项的覆盖不够,它还有哪些问题呢?参考上文已经提到了 “好” 问题的标准,我觉得其中的这样两条是违背的:

从模糊到清晰。显然,问题给出的时候就已经相对比较清晰了,这样的方式并不能模拟软件工程师日常面对的许多模糊而困难的实际问题。我已经提到,对于经验丰富的候选人来说,这样的问题无法重现将实际问题映射到软件问题这样的一个重要思考过程。

另一个是,围绕问题的解决要完整。示例问题的考察过程中,只有缺乏互动的编码环节,没有其它过程,没有编码前的分析、思考和优化,也没有编码后的测试、改进和优化。考虑到 LRU 完整算法是一个实现代码量偏大的问题,拿来放到面试中做一个完整实现,由于会消耗过多的时间,挤压其它时间,因此显得有些过了。

其它 “不好” 的问题

前面说了过于清晰的纯算法题的 “不好” 之处,也说了 “好” 问题的例子,最后我想再来说几类其它的且典型的 “不好” 的例子。

依赖于语言或框架

而这里说的依赖不好,是因为考虑到候选人不同的技术背景,如果没有特殊的需要,避免这样的依赖,以避免产生不应有的错误的衡量数据。

比方说,问一个关于 JVM 的问题,这个问题可以问,特别是在候选人强调其具备一定 Java 背景的前提下。但是这样的问题不能成为 “主菜”,尤其是候选人不一定具备很强的 Java 背景,这样的方式会导致考察的偏颇。

过于复杂的规则或背景知识

这主要是基于面试的有限时间考虑的,过于复杂的规则或背景知识,容易把时间消耗到澄清它们上面;另外,有些背景知识并非是所有人都熟知的,这就会引起考察标准的非预期。我给一个经典的反面例子:

设计一个算法,把一个小于一百万的正整数,使用罗马数字来表示。

这个问题描述简洁,但是拿来做技术面试中的主要问题,其中一个不妥之处就是,不是所有人都很清楚一百万内的罗马数字表示法的。对于不清楚的人来说,要搞清楚这个表示法的规则,就已经是一件有些复杂和耗时的事情了。显然,这个罗马数字表示法的知识点,不应该成为考察和衡量的标准。

当然,有时候有些问题的背景知识是冷门的,但是表述简洁,那么这样的话只要 面试官能够主动、迅速地说清楚,拿来使用是没问题的。

知识性问题

我想特别谈一谈,对于知识性问题的提问。在讨论问题的过程中,如果涉及到关联的具体知识,那么提问一些基础知识是一个不错的选择,这是考察候选人 CS 基础是否牢靠的方式。比如候选人提到使用 HashMap,而他/她最熟悉的编程语言是 C++,那么询问在 C++中 HashMap 是怎样实现的就是一个可行的问题。而且,由于这样的问题是嵌套在前述的这个大流量控制问题的解决过程中的,更显得自然而不突兀。

反之,如果这样的知识性问题较为冷门或浅表,和当前的讨论中的问题无关,或是不在候选人的知识体系内,这样的问题就值得商榷了。比如,仅仅因为自己的项目组使用 Spring,面试官就询问候选人 Spring 的 bean 的单例和多例分别是怎样配置的,而恰巧候选人又是 C++背景,对于这部分并不熟悉,这样的问题除了给候选人造成困扰以外,意义就不太大了。

如果把知识性问题作为主要问题来提问,我认为是不适合的。因为一方面它不具备普适性,另一方面它又具备太强的随机性。

这里不具备普适性这一条,指的是不同技术背景的软件工程师候选人都要能够适合参与这个问题的解答。举例来说,如果问 “Tomcat 的线程池的配置策略?”,这就是一个不具备普适性的问题,这是一个偏重 “知识性” 的问题,如果候选人没有使用过 Tomcat,或是只是略有了解,很可能就栽了。

而具备太强的随机性这一条,指的是一旦待考察的问题具体以后,这个问题就容易从对 “能力” 的考察变成了对 “知识” 的考察,而这个知识,又恰恰是比较容易随机的。也就是说,我们不希望候选人 “恰巧” 知道或不知道某一个细小的知识点,来决定他/她是否通过这项考察。

无论如何,知识性的问题作为考察的辅助方式可以,但不应成为主角。我还是觉得我们应当能把更多的时间,留给主要问题的解决过程本身。

关于 “技术面试中,什么样的问题才是好问题?”,我就说这么多吧。也欢迎你分享你的看法,我们一起讨论。

文章未经特殊标明皆为本人原创,未经许可不得用于任何商业用途,转载请保持完整性并注明来源链接 《四火的唠叨》

via 四火的唠叨 https://ift.tt/38fQVuV

VN:F [1.9.22_1171]
Rating: 0.0/5 (0 votes cast)
VN:F [1.9.22_1171]
Rating: 0 (from 0 votes)
Share

维基百科2020年2月12日典范条目

PatNixon.jpg

塞尔玛·凯瑟琳·“帕特”·瑞安·尼克松第37任美国总统理查德·尼克松的妻子,于1969至1974年间担任第一夫人。帕特·尼克松生于内华达州伊利。1940年,她嫁给了当时还是律师的理查德·尼克松。在1968年美国总统选举成为第一夫人后,帕特·尼克松推动了包括志愿服务在内的多项慈善活动。她陪同丈夫一起出访中华人民共和国苏联,而独立出访非洲和南美洲更带来了“夫人大使”的声誉。她还是第一位进入过战斗区域的第一夫人。这些访问为她赢得了媒体和受访国的好评。她担任第一夫人直到丈夫因水门事件而于1974年辞职时为止。在之后的生活中,她与丈夫回到加利福尼亚州,之后搬到新泽西州。1976年和1983年,她出现了两次中风。1992年,她被诊断出患有肺癌,于1993年逝世,享年81岁。

February 12, 2020 at 08:00AM
from 维基百科典范条目供稿 https://ift.tt/2UUL0Yz

VN:F [1.9.22_1171]
Rating: 0.0/5 (0 votes cast)
VN:F [1.9.22_1171]
Rating: 0 (from 0 votes)
Share

维基百科2020年2月11日典范条目

Governorates of Lebanon.svg

黎巴嫩的行政區劃共有三級,其中省屬於第一級行政區劃,而縣和市鎮則分別作為與第二級行政區劃與第三級行政區劃。黎巴嫩各省由總督管治,而各省總督由黎巴嫩部長會議任命。根據《2003年7月16日第522號法令》,阿卡省與巴勒貝克-希爾米勒省分別從北黎巴嫩省和貝卡省析出,使黎巴嫩管轄的省份的數目由6個增加至8個。在黎巴嫩的8個省中,除作為首都的貝魯特省不轄縣、阿卡省僅轄一個縣(阿卡縣)外,所有省均下轄多於一個縣,而下轄縣數量最多的省為北黎巴嫩省黎巴嫩山省,均轄6個縣。在黎巴嫩的8個省中,除貝魯特省不轄市鎮外,所有省均有下轄市鎮,而下轄市鎮數量最多的省為黎巴嫩山省,轄326個市鎮。在黎巴嫩的8個省中,人口最少的是阿卡省,人口僅有281,843人,而人口最多的是黎巴嫩山省,人口達1,101,817人;面積最小的是貝魯特省,面積僅19.8平方公里,而面積最大的是巴勒貝克-希爾米勒省,面積3,009平方公里;人口密度最低的是巴勒貝克-希爾米勒省,平均每一平方公里僅有人口121.473人,而人口密度最高的是貝魯特省,平均每一平方公里有人口36,947.879人。

February 11, 2020 at 08:00AM
from 维基百科典范条目供稿 https://ift.tt/38rLn0F

VN:F [1.9.22_1171]
Rating: 0.0/5 (0 votes cast)
VN:F [1.9.22_1171]
Rating: 0 (from 0 votes)
Share

维基百科2020年2月10日典范条目

1878 three-dollar piece obverse.jpg

3美元金币美国铸币局于1854至1889年间生产的一种金币,由该局首席雕刻师詹姆斯·B·朗埃克设计,1853年2月21日获得授权。硬币正面刻有头戴美洲原住民公主头饰的自由女神头像,背面的中间是面值和年份,周围有玉米小麦棉花烟草组成的花环。部分来源认为,这种金币是为了方便人们大量购买邮票。朗埃克在设计金币时想了多种办法确保新币和2.5美元金币有显著区别,不但采用的坯饼更薄,而且设计图案也是别具一格。金币投产后的第一年就出产了超过10万枚,但其流通程度非常有限。虽然美国西岸部分地区不接受纸币,只能使用金币和银币,但东部的情况截然不同,特别是在经济内战爆发而陷入动荡后,3美元金币基本上在东部的商品交易中难得一见,并且此后再也没有出现过大范围流通。3美元金币于1889年停产,国会再在次年中止其发行授权。这种硬币存在多种年份版本,其中许多的铸造量都很少,但最为罕见的品种还是旧金山铸币局1870年打造的1870-S版,确认存在的仅有一枚。

February 10, 2020 at 08:00AM
from 维基百科典范条目供稿 https://ift.tt/37bFFhT

VN:F [1.9.22_1171]
Rating: 0.0/5 (0 votes cast)
VN:F [1.9.22_1171]
Rating: 0 (from 0 votes)
Share

维基百科2020年2月9日典范条目

Tropical Cyclone 01B 2003.jpg

2003年5月,一场正式名称为“第一号特强气旋风暴鲍勃”的热带气旋斯里兰卡引发56年来最严重的洪涝灾害。系统于5月10日在孟加拉湾上空发展形成,是2003年北印度洋气旋季的首场风暴。由于外界环境有利,系统在向西北方向移动的过程中稳步加强。于5月13日达到持续风速每小时140公里的最高强度,气旋在孟加拉湾中部上空向北飘移,因风切变增多而逐渐减弱。风暴转向东进,于5月16日退化成强烈低气压,然后蜿蜒向东北方向移动并重新增强成气旋风暴。系统从缅甸西部登岸,于次日在陆地上空消散。气旋在孟加拉湾中部基本停止前进期间给斯里兰卡西南部带去倾盆大雨,引发的洪灾和山体滑坡导致2万4750户民宅被毁,另有3万2426套受到一定程度破坏,致使约80万人无家可归。风暴一共造成260人死亡,经济损失约1亿3500万美元。气旋还令印度的安达曼-尼科巴群岛及该国孟加拉湾沿岸地区降下小雨。除此以外,风暴把水分推离印度大陆,可能因此引发旷日持久的热浪,夺走1400人的生命,缅甸也出现暴雨。

February 09, 2020 at 08:00AM
from 维基百科典范条目供稿 https://ift.tt/2Ssudcs

VN:F [1.9.22_1171]
Rating: 0.0/5 (0 votes cast)
VN:F [1.9.22_1171]
Rating: 0 (from 0 votes)
Share

维基百科2020年2月8日典范条目

源於公元一世紀或二世紀的安息帝國陶製人面噴水器

安息帝國古波斯地區主要的政治及文化勢力。由阿爾沙克一世建立。全盛時期的安息帝國疆域北達今土耳其東南的幼發拉底河,東抵伊朗。安息帝國座落在地中海羅馬帝國與中國漢朝之間的貿易路線絲綢之路之上,使帝國成為了商貿中心。法爾斯伊什塔克爾的統治者阿爾達希爾一世叛變,在公元224年殺害了安息帝國最後一位統治者阿爾達班五世。阿爾達希爾一世建立了薩珊王朝,不過安息帝國的分支阿薩息斯王朝則仍在亞美尼亞繼續其統治。安息帝國是一個由不同文化組成的國家,她在很大程度上吸納了包括波斯文化希臘文化及地區文化的藝術、建築、宗教信仰及皇室標記。

February 08, 2020 at 08:00AM
from 维基百科典范条目供稿 https://ift.tt/374Ilhj

VN:F [1.9.22_1171]
Rating: 0.0/5 (0 votes cast)
VN:F [1.9.22_1171]
Rating: 0 (from 0 votes)
Share

维基百科2020年2月7日典范条目

MMI Chapel.jpg

马里昂军事学院于1842年在美國阿拉巴马州马里昂市創校,在该国仅有的4所军事初级学院中历史最为悠久。马里昂军事学院是南方院校协会的成员之一,有“美国伊顿”之称,曾被评为卓越荣誉军事学校、最佳军事初级学院、阿拉巴马州校友收入最高的社区学院等。马里昂的军校学员团绰号猛虎营。自建校一百余年以来,包括一位美利坚联盟国准将在内的美军各个军种共有超过200名将军毕业于该校。1916年,马里昂军事学院成为了首批引进美国陆军陆军预备军官训练团的学校之一。大约在同一时期,又开设了联邦军校的预备课程。二战时期,学校从仅有的两栋建筑逐步扩建至今日之规模。1968年,美国陆军提前授衔计划进入了马里昂军事学院。2006年,马里昂转型为州立,并成为了阿拉巴马州法定的官方军校。马里昂军事学院的校园經相关机构认证为阿拉巴马州历史地标。校内有两处国家史迹名录,分别是军校礼拜堂和拉夫雷斯楼和校长官邸。此外,阿拉巴马军事名人堂也位于校内。马里昂军事学院每年都会以主要游行队伍的身份参加美国历史最悠久的老兵节庆祝活动——伯明翰老兵节游行。

February 07, 2020 at 08:00AM
from 维基百科典范条目供稿 https://ift.tt/2SnXULI

VN:F [1.9.22_1171]
Rating: 0.0/5 (0 votes cast)
VN:F [1.9.22_1171]
Rating: 0 (from 0 votes)
Share

维基百科2020年2月6日典范条目

Karl Marx 001.jpg

卡爾·馬克思猶太裔德國哲學家經濟學家社會學家政治學家革命理論家新聞從業員歷史學者革命社會主義者。馬克思在經濟學上的工作解釋絕大多數工人和資本家間的關係,並且奠定後來諸多經濟思想的基礎。馬克思亦是社會學與社會科學的鼻祖之一,在他一生中出版了大量著作,其中最著名的分別有1848年發表的《共產黨宣言》和1867年至1894年出版的《資本論》。馬克思關於社會、經濟與政治的理論被統稱為馬克思主義,主張人類社會是在控制生產資料的統治階級與提供勞動生產的勞動階級間不斷的階級鬥爭中發展而成。馬克思認為國家是為維護統治階級的利益而運轉,而這又常常被視為大眾的公共意志。他同時也預言如之前存在過的社會經濟體系一樣,資本主義的內部矛盾會導致它自身的滅亡,並會被新的社會主義社會形態所取代;而資產階級和無產階級之間存在的矛盾,將會由工人階級奪取政治權力而終結,最終建立工人自由人聯合體所管理、沒有階級制度的共產主義社會。

February 06, 2020 at 08:00AM
from 维基百科典范条目供稿 https://ift.tt/39cEujI

VN:F [1.9.22_1171]
Rating: 0.0/5 (0 votes cast)
VN:F [1.9.22_1171]
Rating: 0 (from 0 votes)
Share

维基百科2020年2月5日典范条目

1981-S SBA$ Type Two Deep Cameo.jpg

蘇珊·安東尼銀元是1979至1981年間出產的一種1美元硬幣,1981年時因公眾反響不佳而停產,1999年又再度生產。這種硬幣的誕生是為取代過於臃腫的艾森豪威爾銀元,鑄幣局測試過多種形狀和材質,但都遭到當時對鑄幣立法有強大影響的自動售貨機製造業遊說集團反對,最終決定採用的一種內邊框有11個面的圓形坯餅。硬幣由美國鑄幣局首席雕刻師弗蘭克·加斯帕羅設計,他起初設計的正面是自由女神頭像,但多位國會議員和多個組織呼籲在硬幣上描繪一位真實存在過的美國女性,最終入選為硬幣主題的民權活動家蘇珊·安東尼。硬幣反面則根據國會法案要求,保留艾森豪威爾銀元的設計。由於流通量小,大部分安東尼銀元成色甚佳,但這也導致它們缺乏收藏價值,價格比面值高不了多少。鑄幣局打造有多個年份的精製幣,其中個別版本或品種價格更高。

February 05, 2020 at 08:00AM
from 维基百科典范条目供稿 https://ift.tt/3blCjMC

VN:F [1.9.22_1171]
Rating: 0.0/5 (0 votes cast)
VN:F [1.9.22_1171]
Rating: 0 (from 0 votes)
Share