第 46 期:战战兢兢,如临深渊,如履薄冰

@Author : Lewis Tian (taseikyo@gmail.com)

@Link : github.com/taseikyo

@Range : 2021-09-12 - 2021-09-18

Weekly #46

readme | previous | next

本文总字数 2439 个,阅读时长约:4 分 45 秒,统计数据来自:算筹字数统计

*Photo by ann vahurova on Unsplash

Table of Contents

  • review

    • 绝对值计算并不简单(英文)

    • 为什么大多数软件工程师更喜欢 mac(Medium :-1:)

    • 用 Golang 在秒级读取 16GB 的文件(Medium)

  • tip

    • MDvideo: Markdown To Video

  • share

    • 战战兢兢,如临深渊,如履薄冰

algorithm 🔝

review 🔝

计算一个数的绝对值的问题是完全微不足道的。如果数字是负的,改变符号。否则,就让它保持原样。

在 Java 中,它可能看起来像这样:

public static double abs(double value) {
  if (value < 0) {
    return -value;
  }
  return value;
}

IEEE-754 标准中,特别是在 Java 中,有两个零: +0.0-0.0,把 1.0 除以 +0.0-0.0,你会得到完全不同的答案: +Infinity 和 ,在比较操作中,+0.0-0.0 是无法区分的。因此,上面的实现 -0.0 的转正:

double x = -0.0;
if (1 / abs(x) < 0) {
  System.out.println("oops");
}

直接改为 if (value < 0 | | value =-0.0) 是不行的,因为 +0.0 = -0.0

为了可靠地区分他们俩,有一个 double.compare 方法:

public static double abs(double value) {
  if (value < 0 || Double.compare(value, -0.0) == 0) {
    return -value;
  }
  return value;
}

上述方法虽然有效,但是对于这样一个简单的操作变得非常慢

实现 double.compare 并不是那么简单。对于正数需要两次额外的比较,对于 -0.0 需要三次比较,对于 +0.0 需要多达四次比较。

如果看下 double.compare 的源码,可以看到实际上我们仅需要一个 doubleToLongBits,此方法将 double 类型的二进制表示重新解释为 long 类型:

private static final long MINUS_ZERO_LONG_BITS =
  Double.doubleToLongBits(-0.0);

public static double abs(double value) {
  if (value < 0 ||
      Double.doubleToLongBits(value) == MINUS_ZERO_LONG_BITS) {
    return -value;
  }
  return value;
}

doubleToLongBits 将 NaN 规范化(canonicalizes)了,doubleToLongBits 将 NaN 转化为 0x7ff8000000000l,很显然又多了条件判断。

还有另一种方法 doubleToRawLongBits。它不执行任何智能的 NaN 转换,只是返回完全相同的位表示:

private static final long MINUS_ZERO_LONG_BITS =
  Double.doubleToRawLongBits(-0.0);

public static double abs(double value) {
  if (value < 0 ||
      Double.doubleToRawLongBits(value) == MINUS_ZERO_LONG_BITS) {
    return -value;
  }
  return value;
}

JIT 编译器可以完全删除 doubleToRawLongBits 方法调用,因为这只是重新解释 CPU 寄存器中存储的比特集的问题,这样 Java 数据类型就会一致。

但是比特本身保持不变,CPU 通常不关心数据类型。虽然有传言说,这个电话仍然可能导致从一个浮点寄存器转移到一个通用寄存器。尽管如此,它还是非常快。

我们能做得更少吗?

事实证明,我们可以从 0.0 中减去 0,从而把正的和负的零都变成正的:

System.out.println(0.0-(-0.0)); // 0.0
System.out.println(0.0-(+0.0)); // 0.0

因此,我们可以用以下方法重写实现:

public static double abs(double value) {
  if (value <= 0) {
    return 0.0 - value;
  }
  return value;
}

如果我们观察 IEEE-754 格式的 双精度的二进制表示,我们可以看到符号只是一个最高有效位。因此,我们只需要无条件地清除这个 bit。

在此操作期间,数字的其余部分不会更改。在这方面,小数甚至比整数更简单,负数通过二的补数变为正数。当然,我们还需要重新解释一个双重数字,并且在操作之后重新解释它:

public static double abs(double value) {
  return Double.longBitsToDouble(
    Double.doubleToRawLongBits(value) & 0x7fffffffffffffffL);
}

垃圾文章也要会员就离谱。

作者说了下为什么开发者更喜欢 Mac 的 5 个理由:

1、苹果的生态

2、苹果的操作系统

3、可靠性

这一点真没体会到,已经收到两封安全部门的邮件通知赶紧升级,有高危漏洞。。

4、设计

设一点确实,UI 和交互确实吊打 Windows

5、独占软件

这一点应该骂苹果才对

当今世界的任何计算机系统每天都产生大量的日志或数据。随着系统的发展,将调试数据存储到数据库中是不可行的,因为它们是不可变的,而且只能用于分析和故障解决目的。因此,组织倾向于将其存储在驻留在本地磁盘存储器中的文件中。

首先用标准的 Go 的 os.File 来处理文件 IO:

f, err := os.Open(fileName)
 if err != nil {
   fmt.Println("cannot able to read the file", err)
   return
 }
// UPDATE: close after checking error
defer file.Close()  //Do not forget to close the file

一旦打开文件,有两个选项继续:

1、逐行读取文件,这有助于减少对内存的压力,但在 i/o 中需要更多的时间

2、立即将整个文件读入内存并处理该文件,这将消耗更多内存,且会显著增加时间

由于文件太大,不可能将整个文件加载到内存中

但是第一种选择也是不可行的,因为想在秒级处理文件

还有第三种选择:使用 bufio.NewReader() 分块加载文件:

r := bufio.NewReader(f)
for {
buf := make([]byte,4*1024) //the chunk size
n, err := r.Read(buf) //loading chunk into buffer
buf = buf[:n]
if n == 0 {
    if err != nil {
        fmt.Println(err)
        break
     }
    if err == io.EOF {
        break
    }
    return err
  }
}

当读取了一块,可以起一个线程来处理,这样就可以同时处理每个块:

//sync pools to reuse the memory and decrease the preassure on //Garbage Collector
linesPool := sync.Pool{New: func() interface{} {
    lines := make([]byte, 500*1024)
    return lines
}}
stringPool := sync.Pool{New: func() interface{} {
    lines := ""
    return lines
}}
slicePool := sync.Pool{New: func() interface{} {
    lines := make([]string, 100)
    return lines
}}
r := bufio.NewReader(f)
var wg sync.WaitGroup //wait group to keep track off all threads
for {
    buf := linesPool.Get().([]byte)
    n, err := r.Read(buf)
    buf = buf[:n]
    if n == 0 {
        if err != nil {
            fmt.Println(err)
            break
        }
        if err == io.EOF {
            break
        }
        return err
     }
    nextUntillNewline, err := r.ReadBytes('\n')//read entire line

    if err != io.EOF {
        buf = append(buf, nextUntillNewline...)
    }

    wg.Add(1)
    go func() {

        //process each chunk concurrently
        //start -> log start time, end -> log end time

        ProcessChunk(buf, &linesPool, &stringPool, &slicePool, start, end)
        wg.Done()

    }()
}

wg.Wait()
}

利用了多线程(goroutine)之后,处理时间为 25 秒。

实际上作者可以试试单线程,然后对比一下时间改进(

代码:process_16gb_file.go

tip 🔝

一个将 markdown 文档转为视频的便捷工具

share 🔝

1. 战战兢兢,如临深渊,如履薄冰

曾子有疾,召门弟子曰:“启予足!启予手!诗云:‘战战兢兢,如临深渊,如履薄冰。’而今而后,吾知免夫,小子!”

曾子病危,把学生们召集起来说:“把我的脚露出来,把我的手露出来。《诗经》上说:‘要小心谨慎做事啊,就像在深水潭边一样,就像在薄冰上行走一样。’从今往后,我知道我不会犯错了,学生们。”

生而在世,既不是嬉戏玩乐,也就不能轻易斫(zhuo)杀、轻易取舍,当是如临深渊,如履薄冰。

readme | previous | next

最后更新于

这有帮助吗?