计算机知识

计算机知识

LeetCode刷题Day2:两数相加

LeetCodezkbhj 发表了文章 • 0 个评论 • 65 次浏览 • 2020-09-22 20:57 • 来自相关话题

两数相加

给出两个 非空 的链表用来表示两个非负的整数。其中,它们各自的位数是按照 逆序 的方式存储的,并且它们的每个节点只能存储 一位 数字。如果,我们将这两个数相加起来,则会返回一个新的链表来表示它们的和。 

您可以假设除了数字 0 之外,这两个数都不会以 0 开头。

示例:

输入:(2 -> 4 -> 3) + (5 -> 6 -> 4) 
输出:7 -> 0 -> 8
原因:342 + 465 = 807LeetCode

 首先发一下我的思路已经给出的最初答案(大部分情况是正确的,但是超出数字范围就会和预期结果不一致),总之先把分析思路分享出来。


思路一:常规思路

分别循环两个链表,因为高位是个位,所以不断乘以10的n次方,n代表循环次数,从0开始,直到循环结束,就得到了两个无符号整数。所以,这就要求对输入数字有范围限制的要求,否则数字太大,超出整数的最大范围,得到的结果会因为发生溢出而不符合预期。

然后将两个整数进行加法运算,得到最终结果对应的int数字。

然后再将这个整数转化成字符串,循环字符串,得到字符对应的ASCII码点,然后减去48即可“间接“得到对应的数字。并按序生成链表。
 
func myPow(a,n int) int {
result := int(1)
for i:= n; i >0 ; i >>= 1 {
if i&1 != 0 {
result *=a
}
a *=a
}
return result
}

func addTwoNumbers(l1 *ListNode, l2 *ListNode) *ListNode {
var a,b,c,i,j int
p1 := l1
for p1!= nil {
a = a + p1.Val * myPow(10,i)
i++
p1 = p1.Next
}
p2 := l2
for p2!= nil {
b = b + p2.Val * myPow(10,j)
j++
p2 = p2.Next
}
c = a + b

str := strconv.Itoa(c)
headNode := new(ListNode)
current := headNode
current.Next = nil
for _,one := range str {
preNode := current
current.Val = int(one-48)
current = new(ListNode)
current.Next = preNode

}

return current.Next
}
思路一:高级思路(学习)
对应位置的数字相加,跟10取模,即可得到该位置上十进制数相加之后的数字,然后和10进行整除,如果两个数相加小于10,则整除之后是0,否则是1,即要进位的1,所以直到两个链表都循环完,或者最后没有进位时,停止循环,最终返回。
func addTwoNumbers(l1 *ListNode, l2 *ListNode) *ListNode {
headList := new(ListNode)
head := headList
num := 0

for (l1 != nil || l2 != nil || num > 0) {
headList.Next = new(ListNode)
headList = headList.Next
if l1 != nil {
num = num + l1.Val
l1 = l1.Next
}
if l2 != nil {
num = num + l2.Val
l2 = l2.Next
}
headList.Val = (num) % 10
num = num / 10
}

return head.Next
}
  查看全部


两数相加

给出两个 非空 的链表用来表示两个非负的整数。其中,它们各自的位数是按照 逆序 的方式存储的,并且它们的每个节点只能存储 一位 数字。如果,我们将这两个数相加起来,则会返回一个新的链表来表示它们的和。 

您可以假设除了数字 0 之外,这两个数都不会以 0 开头。

示例:

输入:(2 -> 4 -> 3) + (5 -> 6 -> 4) 
输出:7 -> 0 -> 8
原因:342 + 465 = 807LeetCode


 首先发一下我的思路已经给出的最初答案(大部分情况是正确的,但是超出数字范围就会和预期结果不一致),总之先把分析思路分享出来。


思路一:常规思路

分别循环两个链表,因为高位是个位,所以不断乘以10的n次方,n代表循环次数,从0开始,直到循环结束,就得到了两个无符号整数。所以,这就要求对输入数字有范围限制的要求,否则数字太大,超出整数的最大范围,得到的结果会因为发生溢出而不符合预期。

然后将两个整数进行加法运算,得到最终结果对应的int数字。

然后再将这个整数转化成字符串,循环字符串,得到字符对应的ASCII码点,然后减去48即可“间接“得到对应的数字。并按序生成链表。
 
func myPow(a,n int) int {
result := int(1)
for i:= n; i >0 ; i >>= 1 {
if i&1 != 0 {
result *=a
}
a *=a
}
return result
}

func addTwoNumbers(l1 *ListNode, l2 *ListNode) *ListNode {
var a,b,c,i,j int
p1 := l1
for p1!= nil {
a = a + p1.Val * myPow(10,i)
i++
p1 = p1.Next
}
p2 := l2
for p2!= nil {
b = b + p2.Val * myPow(10,j)
j++
p2 = p2.Next
}
c = a + b

str := strconv.Itoa(c)
headNode := new(ListNode)
current := headNode
current.Next = nil
for _,one := range str {
preNode := current
current.Val = int(one-48)
current = new(ListNode)
current.Next = preNode

}

return current.Next
}

思路一:高级思路(学习)
对应位置的数字相加,跟10取模,即可得到该位置上十进制数相加之后的数字,然后和10进行整除,如果两个数相加小于10,则整除之后是0,否则是1,即要进位的1,所以直到两个链表都循环完,或者最后没有进位时,停止循环,最终返回。

func addTwoNumbers(l1 *ListNode, l2 *ListNode) *ListNode {
headList := new(ListNode)
head := headList
num := 0

for (l1 != nil || l2 != nil || num > 0) {
headList.Next = new(ListNode)
headList = headList.Next
if l1 != nil {
num = num + l1.Val
l1 = l1.Next
}
if l2 != nil {
num = num + l2.Val
l2 = l2.Next
}
headList.Val = (num) % 10
num = num / 10
}

return head.Next
}

 

LeetCode刷题Day1:两数之和

LeetCodezkbhj 发表了文章 • 0 个评论 • 60 次浏览 • 2020-09-22 20:55 • 来自相关话题

从今天开始,把LeetCode刷题作为每天日常计划的一项。尽早做准备,同时也每天对算法和相关的技能知识通过刷题的形式进行潜移默化的提升和加深理解。其实身边已经有很多程序猿朋友都在LeetCode刷题了,再不参与进来,都要出圈儿了呀。

今天是Day1,从第一道题目开始:
 

两数之和

给定一个整数数组 nums 和一个目标值 target,请你在该数组中找出和为目标值的那 两个 整数,并返回他们的数组下标。你可以假设每种输入只会对应一个答案。但是,数组中同一个元素不能使用两遍。 

示例: 

给定 nums = [2, 7, 11, 15], target = 9 

因为 nums[0] + nums[1] = 2 + 7 = 9 所以返回 [0, 1]LeetCode
 

 
思路一:暴力遍历

也是最简单最原始的未经优化的方案思路。

首先明确题目要求,可以假设每种输入只会对应一个答案,即一定会有且只有一对符合要求的数据。

所以得到数组的长度,然后从第一个开始进行循环,将目标值target减去当前被循环的值,得到与之可以配对的值,然后在数组中进行寻找,如果找到,就返回2个符合要求的值的索引,否则继续下一次寻找,直到找到。

最外层循环的条件中 x&y异或之后如果为1,说明已经找到对应的结果,就不再循环。时间复杂度是O(n²),空间复杂度是O(1)。
func twoSum(nums []int, target int) []int {
len := len(nums)
var x,y int

for i:=0; i < len && x|y == 0; i++ {
findNum := target - nums[i]
for j:=0; j < len; j++ {
if findNum == nums[j] {
x = i;
y = j;
}
}

}

return []int{x,y}
}
思路二:进一步提升效率,可以利用哈希表
从第一个思路中,可以看到其实在判断一个值在一个“表”中是否存在的时候,我们采用了遍历的方式,这样的话时间复杂度就是O(n),但是大家肯定都知道,哈希表是最擅长做这个事情的。它可以把时间复杂度降到O(1)。所以,这样思路就很清晰了,我们需要先把传过来的数组生成对应的哈希结构,也就是Go里面的map字典,这样我们就可以快速的找到某值在还是不在了。我们把数组的value作为map的key,index作为map的value。代码实现如下。
 
func twoSum(nums []int, target int) []int {
var hashMap map[int]int
len := len(nums)
var x,y,found int

//初始化字典
hashMap = make(map[int]int)
for i:=0; i < len; i++ {
hashMap[nums[i]] = i
}

for j:=0; j < len && found == 0; j++ {
findNum := target - nums[j]
v, ok := hashMap[findNum]
if ok && v != j {
found = 1
x = j
y = v
}

}

return []int{x,y}
}
今天这道题的难度级别是简单,但是带我回顾了Go语言的数组、map的相关知识和用法,以及哈希表这一知识点。 查看全部
从今天开始,把LeetCode刷题作为每天日常计划的一项。尽早做准备,同时也每天对算法和相关的技能知识通过刷题的形式进行潜移默化的提升和加深理解。其实身边已经有很多程序猿朋友都在LeetCode刷题了,再不参与进来,都要出圈儿了呀。

今天是Day1,从第一道题目开始:
 


两数之和

给定一个整数数组 nums 和一个目标值 target,请你在该数组中找出和为目标值的那 两个 整数,并返回他们的数组下标。你可以假设每种输入只会对应一个答案。但是,数组中同一个元素不能使用两遍。 

示例: 

给定 nums = [2, 7, 11, 15], target = 9 

因为 nums[0] + nums[1] = 2 + 7 = 9 所以返回 [0, 1]LeetCode
 


 
思路一:暴力遍历

也是最简单最原始的未经优化的方案思路。

首先明确题目要求,可以假设每种输入只会对应一个答案,即一定会有且只有一对符合要求的数据。

所以得到数组的长度,然后从第一个开始进行循环,将目标值target减去当前被循环的值,得到与之可以配对的值,然后在数组中进行寻找,如果找到,就返回2个符合要求的值的索引,否则继续下一次寻找,直到找到。

最外层循环的条件中 x&y异或之后如果为1,说明已经找到对应的结果,就不再循环。时间复杂度是O(n²),空间复杂度是O(1)。

func twoSum(nums []int, target int) []int {
len := len(nums)
var x,y int

for i:=0; i < len && x|y == 0; i++ {
findNum := target - nums[i]
for j:=0; j < len; j++ {
if findNum == nums[j] {
x = i;
y = j;
}
}

}

return []int{x,y}
}

思路二:进一步提升效率,可以利用哈希表
从第一个思路中,可以看到其实在判断一个值在一个“表”中是否存在的时候,我们采用了遍历的方式,这样的话时间复杂度就是O(n),但是大家肯定都知道,哈希表是最擅长做这个事情的。它可以把时间复杂度降到O(1)。所以,这样思路就很清晰了,我们需要先把传过来的数组生成对应的哈希结构,也就是Go里面的map字典,这样我们就可以快速的找到某值在还是不在了。我们把数组的value作为map的key,index作为map的value。代码实现如下。
 

func twoSum(nums []int, target int) []int {
var hashMap map[int]int
len := len(nums)
var x,y,found int

//初始化字典
hashMap = make(map[int]int)
for i:=0; i < len; i++ {
hashMap[nums[i]] = i
}

for j:=0; j < len && found == 0; j++ {
findNum := target - nums[j]
v, ok := hashMap[findNum]
if ok && v != j {
found = 1
x = j
y = v
}

}

return []int{x,y}
}

今天这道题的难度级别是简单,但是带我回顾了Go语言的数组、map的相关知识和用法,以及哈希表这一知识点。

使用了Go mod管理的项目里如何添加新的第三方包?

回复

GoLangzkbhj 回复了问题 • 1 人关注 • 1 个回复 • 175 次浏览 • 2020-09-17 11:32 • 来自相关话题

如何快速提升Go程序性能?

GoLangzkbhj 发表了文章 • 0 个评论 • 90 次浏览 • 2020-09-15 18:39 • 来自相关话题

这篇文章中列出了一些不需要太多精力就能显著提高性能的技巧,并不包含那些需要太多精力或需要大幅度修改程序结构的技巧。
 
开始优化之前
 
开始优化之前,首先应该花些时间找出一个合适的基准线,以便稍后比较。如果没有基准,那就等于摸着石头过河,根本不知道自己的优化有没有效果。首先要编写性能测试程序,然后生成能用于pprof的profile文件。最好可以编写Go的性能测试脚本(https://dave.cheney.net/2013/0 ... in-go),这样可以很容易地使用pprof,还可以评测内存分配情况。还可以使用benchcmp,这个工具可以帮助比较两次性能测试之间的性能差异。

如果代码很难做性能测试,那就从你能测量时间的部分开始。可以利用runtime/pprof手工测量代码。

现在开始吧!
 
使用sync.Pool重用之前分配过的对象
 
sync.Pool实现了一个空闲列表(free-list)。这样可以重新使用之前分配过的对象。这样做可以将对象分配的代价平摊到多次使用上,减少垃圾回收器的工作。API非常简单:只需实现一个函数,用来分配新的对象即可。它会返回指针类型。
 
var bufpool = sync.Pool{
New: func() interface{} {
buf := make([]byte, 512)
return &buf
}}之后,可以用Get()从池中获取对象,用完之后用Put()将对象放回。

不过要注意一些陷阱。在Go 1.13之前,每次发生垃圾回收时该池都会被清空。对于需要分配大量对象的程序来说,这可能会造成性能的影响。在1.13版本中似乎GC后能保留更多对象了(https://go-review.googlesource.com/c/go/+/162919/)。

你可能需要在将对象放回池中之前将其结构的字段清空。

如果不这样做,就可能从池中获得一个“脏”的对象,它包含之前使用过的数据。这可能会造成严重的安全问题!
 
安全的做法就是明确清空内存:// reset resets all fields of the AuthenticationResponse before pooling it.
func (a* AuthenticationResponse) reset() {
a.Token = ""
a.UserID = ""
}

rsp := authPool.Get().(*AuthenticationResponse)
defer func() {
rsp.reset()
authPool.Put(rsp)
}()
在大map中避免使用包含指针的结构作为map的键
 
关于Go中大型堆的性能问题已经有很多人讨论过了。在垃圾回收过程中,运行时会扫描包含指针的对象并遍历其指针。如果你有非常大的map[string]int,那么垃圾回收器就不得不在每次垃圾回收过程中检查map中的每个字符串,因为字符串包含指针。

这个例子中我们向一个map[string]int中写入了一千万个元素,然后测量垃圾回收的时间。map是在包的作用域中分配的,以保证它被分配到堆上。
import (
"fmt"
"runtime"
"strconv"
"time"
)

const (
numElements = 10000000
)

var foo = map[string]int{}

func timeGC() {
t := time.Now()
runtime.GC()
fmt.Printf("gc took: %s\n", time.Since(t))
}

func main() {
for i := 0; i < numElements; i++ {
foo[strconv.Itoa(i)] = i
}

for {
timeGC()
time.Sleep(1 * time.Second)
}
}运行后可以得到以下结果:gc took: 98.726321ms
gc took: 105.524633ms
gc took: 102.829451ms
gc took: 102.71908ms
gc took: 103.084104ms
gc took: 104.821989ms对于计算机来说这花得时间太多了!

怎样可以改进呢?最好是能尽量去掉指针,这样能减少垃圾回收器需要遍历的指针数量。由于字符串包含指针,因此我们可以用map[int]int来实现:
 
import (
"fmt"
"runtime"
"time"
)

const (
numElements = 10000000
)

var foo = map[int]int{}

func timeGC() {
t := time.Now()
runtime.GC()
fmt.Printf("gc took: %s\n", time.Since(t))
}

func main() {
for i := 0; i < numElements; i++ {
foo[i] = i
}

for {
timeGC()
time.Sleep(1 * time.Second)
}
}重新运行程序,结果如下:
 
gc took: 3.608993ms
gc took: 3.926913ms
gc took: 3.955706ms
gc took: 4.063795ms
gc took: 3.91519ms
gc took: 3.75226ms好多了。垃圾回收的时间减少了97%。在生产环境下,字符串需要进行hash之后再插入到map中。
 
生成marshalling代码以避免运行时反射
 
将数据结构marshalh或unmarshal成JSON等各种序列化格式是个很常见的操作,特别是在构建微服务的时候。实际上,大部分微服务做的唯一工作就是序列化。像json.Marshal和json.Unmarshal需要依赖运行时反射才能将结构体的字段序列化成字节,反之亦然。这个操作很慢,反射的性能完全无法与显式的代码相比。

但我们不必这么做。marshalling JSON的原理大致如下:
package json

// Marshal take an object and returns its representation in JSON.
func Marshal(obj interface{}) ([]byte, error) {
// Check if this object knows how to marshal itself to JSON
// by satisfying the Marshaller interface.
if m, is := obj.(json.Marshaller); is {
return m.MarshalJSON()
}

// It doesn't know how to marshal itself. Do default reflection based marshallling.
return marshal(obj)
}如果我们知道怎样将对象marshal成JSON,就应该避免运行时反射。但我们不想手工marshal所有代码,怎么办呢?可以让计算机替我们写程序!像easyjson等代码生成器会检查结构体,然后生成高度优化且与json.Marshaller等接口完全兼容的代码。

下载这个包,然后在包含结构体的$file.go上运行下面的命令:
easyjson -all $file.go$file.go
这个命令会生成$file_easyjson.go。由于easyjson为我们实现了json.Marshaller接口,因此序列化时不会调用默认的反射,而是会使用生成的函数。祝贺你!你已经将JSON marshalling的代码的速度提高了三倍。还有许多其他技巧可以进一步提升性能。
 
使用strings.Builder来构建字符串
 
Go语言的字符串是不可修改的,可以认为它们是只读的字节切片。这就是说,每次创建字符串都要分配新的内存,可能还会给垃圾回收器造成更多工作。

Go 1.10引入了strings.Builder作为高效率构建字符串的方式。它内部会将字符串写入到字节缓冲区。只有在builder上调用String()时才会真正生成字符串。它依赖一些unsafe的技巧将底层的字节作为字符串返回,而不实际进行内存非配。这篇文章(https://syslog.ravelin.com/byt ... ca7ff)介绍了更多其工作原理。pkg: github.com/sjwhitworth/perfblog/strbuild
BenchmarkStringBuildNaive-8 5000000 255 ns/op 216 B/op 8 allocs/op
BenchmarkStringBuildBuilder-8 20000000 54.9 ns/op 64 B/op 1 allocs/op可见,strings.Builder要快4.7倍,它的内存分配次只有前者的1/8,内存使用只有前者的1/4。

所以,在性能重要的时候应该使用strings.Builder。一般来说,除非是非常不重要的情况,否则我建议永远使用strings.Builder来构建字符串。
 
使用strconv代替fmt

fmt是Go中最著名的包之一。估计你从第一个Go程序——输出“hello, world”——的时候就开始用它了。但是,如果需要将整数和浮点数转换成字符串,它就不如更底层的strconv有效率了。strconv只需要在API中进行很小改动,就能带来不错的性能提升。

大多数情况下fmt会接受一个interface{}作为参数。这样做有两个弊端:

失去了类型安全。对于我来说这是个很大的问题。

会增加内存分配次数。将非指针类型作为interface{}传递通常会导致堆分配的问题。进一步的内容可以阅读这篇文章(https://www.darkcoding.net/sof ... face/)。
BenchmarkStrconv-8 30000000 39.5 ns/op 32 B/op 1 allocs/op
BenchmarkFmt-8 10000000 143 ns/op 72 B/op 3 allocs/op可以看到,strconv版本要快3.5倍,内存分配次数是1/3,内存分配量是1/2。

在make中指定分配的容量来避免重新分配

在讨论性能改善之前,我们先来迅速看一下切片。切片是Go语言中一个非常有用的概念。它提供了可改变大小的数组,还可以用不同的方式表示同一片底层内存区域,而不需要重新进行内存分配。slice的内部结构由三个元素组成:

data:切片中指向底层数据的指针
len:切片中的当前元素数目
cap:在不重新分配内存的前提下,切片能够增长到的元素数目

我经常看到类似于下面的代码,尽管在切片容量可以预先得知的情况下依然生成一个容量为零的切片:var userIDs []string
for _, bar := range rsp.Users {
userIDs = append(userIDs, bar.ID)
}这段代码中,切片的初始长度和容量都为零。在收到响应后,我们将用户添加到切片。这样做就会达到切片的容量上限,从而导致底层分配两倍容量的新数组,然后将旧切片中的数据拷贝过来。如果有8个用户,就会造成5次内存分配。

更有效的方式是这样:
userIDs := make([]string, 0, len(rsp.Users)
for _, bar := range rsp.Users {
userIDs = append(userIDs, bar.ID)
}使用make显式声明切片的容量。接下来可以向切片添加元素,而不会触发内存重新分配和拷贝。
 
这条建议也适用于map:适用make(map[string]string, len(foo))可以分配足够的底层内存以避免内存重新分配。
 
使用可以接受字节切片的方法
 
在使用包时,寻找那些接受字节切片作为参数的方法,这些方法通常给你更多控制内存分配的自由。

一个很好的例子就是time.Format和time.AppendFormat。time.Format返回字符串。内部会分配一个新的字节切片,然后在其上调用time.AppendFormat。而time.AppendFormat接受一个字节缓冲区,将格式化后的时间写入缓冲区,然后返回扩展后的字节切片。标准库中这种做法非常常见,如strconv.AppendFloat或bytes.NewBuffer。

为什么这样能提高性能?因为你可以传递从sync.Poolh获得的字节切片,而不需要每次都分配新的缓冲区。或者可以初始化一个足够大的缓冲区,来减少切片拷贝。
 
这些建议只是某些具体情况下的建议,而不是真理。一定要自己测量性能。

要知道何时该停止优化。提高系统性能会让工程师感觉非常满足:问题本身很有趣,也有立竿见影的效果。但是,提高性能带来的效果非常依赖于具体情况。如果服务的响应时间只有10毫秒,而网络访问需要90毫秒,那么将10毫秒优化到5毫秒就完全不值得,因为你依然需要95毫秒。就算你将响应时间优化到1毫秒,最后结果还是91毫秒。你应该去做其他更有价值的事情。
 
原文:
https://stephen.sh/posts/quick-go-performance-improvements
译文:
https://blog.csdn.net/csdnnews/article/details/93987866
  查看全部
这篇文章中列出了一些不需要太多精力就能显著提高性能的技巧,并不包含那些需要太多精力或需要大幅度修改程序结构的技巧。
 
开始优化之前
 
开始优化之前,首先应该花些时间找出一个合适的基准线,以便稍后比较。如果没有基准,那就等于摸着石头过河,根本不知道自己的优化有没有效果。首先要编写性能测试程序,然后生成能用于pprof的profile文件。最好可以编写Go的性能测试脚本(https://dave.cheney.net/2013/0 ... in-go),这样可以很容易地使用pprof,还可以评测内存分配情况。还可以使用benchcmp,这个工具可以帮助比较两次性能测试之间的性能差异。

如果代码很难做性能测试,那就从你能测量时间的部分开始。可以利用runtime/pprof手工测量代码。

现在开始吧!
 
使用sync.Pool重用之前分配过的对象
 
sync.Pool实现了一个空闲列表(free-list)。这样可以重新使用之前分配过的对象。这样做可以将对象分配的代价平摊到多次使用上,减少垃圾回收器的工作。API非常简单:只需实现一个函数,用来分配新的对象即可。它会返回指针类型。
 
var bufpool = sync.Pool{
New: func() interface{} {
buf := make([]byte, 512)
return &buf
}}
之后,可以用Get()从池中获取对象,用完之后用Put()将对象放回。

不过要注意一些陷阱。在Go 1.13之前,每次发生垃圾回收时该池都会被清空。对于需要分配大量对象的程序来说,这可能会造成性能的影响。在1.13版本中似乎GC后能保留更多对象了(https://go-review.googlesource.com/c/go/+/162919/)。

你可能需要在将对象放回池中之前将其结构的字段清空。

如果不这样做,就可能从池中获得一个“脏”的对象,它包含之前使用过的数据。这可能会造成严重的安全问题!
 
安全的做法就是明确清空内存:
// reset resets all fields of the AuthenticationResponse before pooling it.
func (a* AuthenticationResponse) reset() {
a.Token = ""
a.UserID = ""
}

rsp := authPool.Get().(*AuthenticationResponse)
defer func() {
rsp.reset()
authPool.Put(rsp)
}()

在大map中避免使用包含指针的结构作为map的键
 
关于Go中大型堆的性能问题已经有很多人讨论过了。在垃圾回收过程中,运行时会扫描包含指针的对象并遍历其指针。如果你有非常大的map[string]int,那么垃圾回收器就不得不在每次垃圾回收过程中检查map中的每个字符串,因为字符串包含指针。

这个例子中我们向一个map[string]int中写入了一千万个元素,然后测量垃圾回收的时间。map是在包的作用域中分配的,以保证它被分配到堆上。
import (
"fmt"
"runtime"
"strconv"
"time"
)

const (
numElements = 10000000
)

var foo = map[string]int{}

func timeGC() {
t := time.Now()
runtime.GC()
fmt.Printf("gc took: %s\n", time.Since(t))
}

func main() {
for i := 0; i < numElements; i++ {
foo[strconv.Itoa(i)] = i
}

for {
timeGC()
time.Sleep(1 * time.Second)
}
}
运行后可以得到以下结果:
gc took: 98.726321ms
gc took: 105.524633ms
gc took: 102.829451ms
gc took: 102.71908ms
gc took: 103.084104ms
gc took: 104.821989ms
对于计算机来说这花得时间太多了!

怎样可以改进呢?最好是能尽量去掉指针,这样能减少垃圾回收器需要遍历的指针数量。由于字符串包含指针,因此我们可以用map[int]int来实现:
 
import (
"fmt"
"runtime"
"time"
)

const (
numElements = 10000000
)

var foo = map[int]int{}

func timeGC() {
t := time.Now()
runtime.GC()
fmt.Printf("gc took: %s\n", time.Since(t))
}

func main() {
for i := 0; i < numElements; i++ {
foo[i] = i
}

for {
timeGC()
time.Sleep(1 * time.Second)
}
}
重新运行程序,结果如下:
 
gc took: 3.608993ms
gc took: 3.926913ms
gc took: 3.955706ms
gc took: 4.063795ms
gc took: 3.91519ms
gc took: 3.75226ms
好多了。垃圾回收的时间减少了97%。在生产环境下,字符串需要进行hash之后再插入到map中。
 
生成marshalling代码以避免运行时反射
 
将数据结构marshalh或unmarshal成JSON等各种序列化格式是个很常见的操作,特别是在构建微服务的时候。实际上,大部分微服务做的唯一工作就是序列化。像json.Marshal和json.Unmarshal需要依赖运行时反射才能将结构体的字段序列化成字节,反之亦然。这个操作很慢,反射的性能完全无法与显式的代码相比。

但我们不必这么做。marshalling JSON的原理大致如下:
package json

// Marshal take an object and returns its representation in JSON.
func Marshal(obj interface{}) ([]byte, error) {
// Check if this object knows how to marshal itself to JSON
// by satisfying the Marshaller interface.
if m, is := obj.(json.Marshaller); is {
return m.MarshalJSON()
}

// It doesn't know how to marshal itself. Do default reflection based marshallling.
return marshal(obj)
}
如果我们知道怎样将对象marshal成JSON,就应该避免运行时反射。但我们不想手工marshal所有代码,怎么办呢?可以让计算机替我们写程序!像easyjson等代码生成器会检查结构体,然后生成高度优化且与json.Marshaller等接口完全兼容的代码。

下载这个包,然后在包含结构体的$file.go上运行下面的命令:
easyjson -all $file.go$file.go

这个命令会生成$file_easyjson.go。由于easyjson为我们实现了json.Marshaller接口,因此序列化时不会调用默认的反射,而是会使用生成的函数。祝贺你!你已经将JSON marshalling的代码的速度提高了三倍。还有许多其他技巧可以进一步提升性能。
 
使用strings.Builder来构建字符串
 
Go语言的字符串是不可修改的,可以认为它们是只读的字节切片。这就是说,每次创建字符串都要分配新的内存,可能还会给垃圾回收器造成更多工作。

Go 1.10引入了strings.Builder作为高效率构建字符串的方式。它内部会将字符串写入到字节缓冲区。只有在builder上调用String()时才会真正生成字符串。它依赖一些unsafe的技巧将底层的字节作为字符串返回,而不实际进行内存非配。这篇文章(https://syslog.ravelin.com/byt ... ca7ff)介绍了更多其工作原理。
pkg: github.com/sjwhitworth/perfblog/strbuild
BenchmarkStringBuildNaive-8 5000000 255 ns/op 216 B/op 8 allocs/op
BenchmarkStringBuildBuilder-8 20000000 54.9 ns/op 64 B/op 1 allocs/op
可见,strings.Builder要快4.7倍,它的内存分配次只有前者的1/8,内存使用只有前者的1/4。

所以,在性能重要的时候应该使用strings.Builder。一般来说,除非是非常不重要的情况,否则我建议永远使用strings.Builder来构建字符串。
 
使用strconv代替fmt

fmt是Go中最著名的包之一。估计你从第一个Go程序——输出“hello, world”——的时候就开始用它了。但是,如果需要将整数和浮点数转换成字符串,它就不如更底层的strconv有效率了。strconv只需要在API中进行很小改动,就能带来不错的性能提升。

大多数情况下fmt会接受一个interface{}作为参数。这样做有两个弊端:

失去了类型安全。对于我来说这是个很大的问题。

会增加内存分配次数。将非指针类型作为interface{}传递通常会导致堆分配的问题。进一步的内容可以阅读这篇文章(https://www.darkcoding.net/sof ... face/)。
BenchmarkStrconv-8      30000000            39.5 ns/op        32 B/op          1 allocs/op
BenchmarkFmt-8 10000000 143 ns/op 72 B/op 3 allocs/op
可以看到,strconv版本要快3.5倍,内存分配次数是1/3,内存分配量是1/2。

在make中指定分配的容量来避免重新分配

在讨论性能改善之前,我们先来迅速看一下切片。切片是Go语言中一个非常有用的概念。它提供了可改变大小的数组,还可以用不同的方式表示同一片底层内存区域,而不需要重新进行内存分配。slice的内部结构由三个元素组成:


data:切片中指向底层数据的指针
len:切片中的当前元素数目
cap:在不重新分配内存的前提下,切片能够增长到的元素数目


我经常看到类似于下面的代码,尽管在切片容量可以预先得知的情况下依然生成一个容量为零的切片:
var userIDs []string
for _, bar := range rsp.Users {
userIDs = append(userIDs, bar.ID)
}
这段代码中,切片的初始长度和容量都为零。在收到响应后,我们将用户添加到切片。这样做就会达到切片的容量上限,从而导致底层分配两倍容量的新数组,然后将旧切片中的数据拷贝过来。如果有8个用户,就会造成5次内存分配。

更有效的方式是这样:
userIDs := make([]string, 0, len(rsp.Users)
for _, bar := range rsp.Users {
userIDs = append(userIDs, bar.ID)
}
使用make显式声明切片的容量。接下来可以向切片添加元素,而不会触发内存重新分配和拷贝。
 
这条建议也适用于map:适用make(map[string]string, len(foo))可以分配足够的底层内存以避免内存重新分配。
 
使用可以接受字节切片的方法
 
在使用包时,寻找那些接受字节切片作为参数的方法,这些方法通常给你更多控制内存分配的自由。

一个很好的例子就是time.Format和time.AppendFormat。time.Format返回字符串。内部会分配一个新的字节切片,然后在其上调用time.AppendFormat。而time.AppendFormat接受一个字节缓冲区,将格式化后的时间写入缓冲区,然后返回扩展后的字节切片。标准库中这种做法非常常见,如strconv.AppendFloat或bytes.NewBuffer。

为什么这样能提高性能?因为你可以传递从sync.Poolh获得的字节切片,而不需要每次都分配新的缓冲区。或者可以初始化一个足够大的缓冲区,来减少切片拷贝。
 
这些建议只是某些具体情况下的建议,而不是真理。一定要自己测量性能。

要知道何时该停止优化。提高系统性能会让工程师感觉非常满足:问题本身很有趣,也有立竿见影的效果。但是,提高性能带来的效果非常依赖于具体情况。如果服务的响应时间只有10毫秒,而网络访问需要90毫秒,那么将10毫秒优化到5毫秒就完全不值得,因为你依然需要95毫秒。就算你将响应时间优化到1毫秒,最后结果还是91毫秒。你应该去做其他更有价值的事情。
 
原文:
https://stephen.sh/posts/quick-go-performance-improvements
译文:
https://blog.csdn.net/csdnnews/article/details/93987866
 

MySQL如何通过sql查询数据的区间分布情况?

回复

数据库zkbhj 回复了问题 • 1 人关注 • 1 个回复 • 165 次浏览 • 2020-09-07 10:42 • 来自相关话题

#面试题集锦#实现单链表反转

面试zkbhj 发表了文章 • 0 个评论 • 134 次浏览 • 2020-08-14 17:16 • 来自相关话题

反转一个链表

示例:输入: 1->2->3->4->5->NULL
输出: 5->4->3->2->1->NULL
结构体定义struct ListNode {
int val;
struct ListNode *next;
};
 





 思路一

先对原链表做头删操作,再对新链表做头插
定义一个新head头指针,标记为newHead,将它初始为NULL,并非指向NULL,最后我们选择返回这个newHead指针作为新链表的头指针。
定义一个结点node作为"临时中转站",初始化与否并无大影响。进行循环遍历链表各个结点,判定head指针是否为空,即是否到达了原链表的结尾。如果不为空,说明还没有到达尾部。如果程序第一次运行就没有进入循环,说明传入了一个空链表,此时返回newHead新链表的头指针,同样返回NULL,这样处理也是合理的。以下开始逆序链表逻辑:在当前指针不为NULL时,先对原链表做头删操作,再对新链表做头插操作。即使用循环进行操作:让node指针指向传入函数链表的头指针head,两指针指向保持相同。然后让head指针指向它的next结点,此时旧链表已经完成了头删操作。第一个结点已经被"切割"下来。接下来要做的就是对新链表进行头插操作,使结点放入新链表。让node指针的next下一个结点指向新链表的头指针newHead,完成结点的链接,即头插入新的链表中。然后更新newHead指向为新链表的头结点。进行下一次循环。最终head指针指向了原链表的结尾,即为NULL,退出循环,此时新链表已经反转完毕,情况如图:最终返回新链表头指针newHead即可。





 struct ListNode *reverseList(struct ListNode* head) {
struct ListNode *newHead = NULL;
struct ListNode *node;
while (head != NULL) {
//1. 对之前的链表做头删
node = head;
head = head->next;

//2. 对新链表做头插
node->next = newHead;
newHead = node;
}
return newHead;
}
思路二





利用选择语句,找到空链表的情况,此情况返回NULL空指针,因为空链表不能反转,或者说反转之后还是一个空链表,返回空。利用三个指针p0"前指针"、p1“当前指针”、p2"后指针"来分批处理链表元素,p0置为NULL,它将作为链表的尾结点向前推进处理,p1指向旧链表的头指针head,p2指向旧链表的头指针的next结点。开始遍历链表,循环判定因子为p1,当它为空时到达链表尾部跳出循环。否则在表中执行循环内逻辑:将p1指向的当前结点的下一个结点指向p0,即前一个结点。此时p0为NULL,那么p1的下一个结点就为空了,它现在是最后一个结点。然后将p0指针指向p1,将p1指针指向p2,注意这两步不可以调换顺序,否则正确向后挪移一位。此时完成了三个指针的一轮更迭。判定p2指针是否为空,如果为空说明此时p2到达了链表结尾,当前指针p1的指向为最后一个结点,它的next即为空。如果不为空,将p2更新到下一个结点,进行下一次循环。下一次进行循环时,就会把截断结点链接到新链表的头部,同时更新三个指针。继续循环。循环终止条件为:p1指向了链表尾部的NULL,此时p1的前指针p0即指向了反转后的链表,它就是新链表的head头指针。此时返回p0即可。





 struct ListNode *reverseList(struct ListNode* head) {
if (head == NULL) {
return NULL;
}
struct ListNode *p0 = NULL;
struct ListNode *p1 = head;
struct ListNode *p2 = head->next;
while (p1 != NULL) {
p1->next = p0;

p0 = p1;
p1 = p2;
if (p2 != NULL) {
p2 = p2->next;
}
}
return p0;
}
参考文档:
https://blog.csdn.net/qq_42351880/article/details/88637387
 
  查看全部
反转一个链表

示例:
输入: 1->2->3->4->5->NULL
输出: 5->4->3->2->1->NULL

结构体定义
struct ListNode {
int val;
struct ListNode *next;
};

 

QQ截图20200814170600.jpg

 思路一

先对原链表做头删操作,再对新链表做头插
  1. 定义一个新head头指针,标记为newHead,将它初始为NULL,并非指向NULL,最后我们选择返回这个newHead指针作为新链表的头指针。

  1. 定义一个结点node作为"临时中转站",初始化与否并无大影响。
  2. 进行循环遍历链表各个结点,判定head指针是否为空,即是否到达了原链表的结尾。如果不为空,说明还没有到达尾部。如果程序第一次运行就没有进入循环,说明传入了一个空链表,此时返回newHead新链表的头指针,同样返回NULL,这样处理也是合理的。
  3. 以下开始逆序链表逻辑:在当前指针不为NULL时,先对原链表做头删操作,再对新链表做头插操作。即使用循环进行操作:
  4. 让node指针指向传入函数链表的头指针head,两指针指向保持相同。
  5. 然后让head指针指向它的next结点,此时旧链表已经完成了头删操作。第一个结点已经被"切割"下来。接下来要做的就是对新链表进行头插操作,使结点放入新链表。
  6. 让node指针的next下一个结点指向新链表的头指针newHead,完成结点的链接,即头插入新的链表中。然后更新newHead指向为新链表的头结点。进行下一次循环。
  7. 最终head指针指向了原链表的结尾,即为NULL,退出循环,此时新链表已经反转完毕,情况如图:
  8. 最终返回新链表头指针newHead即可。


QQ截图20200814170610.jpg

 
struct ListNode *reverseList(struct ListNode* head) {
struct ListNode *newHead = NULL;
struct ListNode *node;
while (head != NULL) {
//1. 对之前的链表做头删
node = head;
head = head->next;

//2. 对新链表做头插
node->next = newHead;
newHead = node;
}
return newHead;
}

思路二

QQ截图20200814171211.jpg

  1. 利用选择语句,找到空链表的情况,此情况返回NULL空指针,因为空链表不能反转,或者说反转之后还是一个空链表,返回空。
  2. 利用三个指针p0"前指针"、p1“当前指针”、p2"后指针"来分批处理链表元素,p0置为NULL,它将作为链表的尾结点向前推进处理,p1指向旧链表的头指针head,p2指向旧链表的头指针的next结点。
  3. 开始遍历链表,循环判定因子为p1,当它为空时到达链表尾部跳出循环。否则在表中执行循环内逻辑:将p1指向的当前结点的下一个结点指向p0,即前一个结点。此时p0为NULL,那么p1的下一个结点就为空了,它现在是最后一个结点。
  4. 然后将p0指针指向p1,将p1指针指向p2,注意这两步不可以调换顺序,否则正确向后挪移一位。此时完成了三个指针的一轮更迭。
  5. 判定p2指针是否为空,如果为空说明此时p2到达了链表结尾,当前指针p1的指向为最后一个结点,它的next即为空。如果不为空,将p2更新到下一个结点,进行下一次循环。
  6. 下一次进行循环时,就会把截断结点链接到新链表的头部,同时更新三个指针。继续循环。
  7. 循环终止条件为:p1指向了链表尾部的NULL,此时p1的前指针p0即指向了反转后的链表,它就是新链表的head头指针。此时返回p0即可。


QQ截图20200814171221.jpg

 
struct ListNode *reverseList(struct ListNode* head) {
if (head == NULL) {
return NULL;
}
struct ListNode *p0 = NULL;
struct ListNode *p1 = head;
struct ListNode *p2 = head->next;
while (p1 != NULL) {
p1->next = p0;

p0 = p1;
p1 = p2;
if (p2 != NULL) {
p2 = p2->next;
}
}
return p0;
}

参考文档:
https://blog.csdn.net/qq_42351880/article/details/88637387
 
 

如何给页面增加文字水印?

回复

前端开发zkbhj 回复了问题 • 1 人关注 • 1 个回复 • 300 次浏览 • 2020-08-06 14:31 • 来自相关话题

#每日精进#2020年8月6日

总结zkbhj 发表了文章 • 0 个评论 • 144 次浏览 • 2020-08-06 09:31 • 来自相关话题

【早读:《深入理解计算机系统》】

第二章 信息的表示和处理

表示代码int sum(int x, int y) {
return x + y;
}当我们在不同的机器上编译上面的程序得到的机器代码都不尽相同:

Linux32     55 89 e5 8b 45 0c 03 45 08 c9 c3
Windows  55 89 e5 8b 45 0c 03 45 08 5d c3
Sun           81 c3 e0 08 90 03 00 09


因此,二进制代码是不兼容的,无法在不同机器之间移植。

这得到一个计算机系统的基本概念:从机器的角度来看,程序仅仅只是字节序列,机器没有关于原始程序的任何信息。

布尔代数简介

计算机的核心都是围绕1和0来演化的。对于0和1的起源,要追溯到1850年前后乔治·布尔的工作,所以这个也叫布尔代数。即通过将逻辑值TRUE和FALSE编码为二进制1和0设计出的一种代数,以研究逻辑推理的基本原则。






上面分别列出了~(NOT)、&(AND)、|(OR)和^(EXCLUSIVE-OR)四种基本运算。

后来创立信息论领域的Claude Shannon首先建立了布尔代数和数字逻辑之间的联系。

将上述基础的布尔运算扩展到位向量运算。位向量就是固定长度为w、由0和1组成的串。

假设 w=4,a=[0110],b=[1100]。那么四种运算 a&b、a|b、a^b、~b 结果分别如下:





 
布尔运算&对|满足分配率:a&(b|c) = (a&b)|(a&c);反过来,|也满足对&的分配率,即:a|(b&c) = (a|b) & (a|c)。

位向量的一个有用应用就是表示有限集合,即用位向量来给集合进行编码。

C语言的一个有用特性就是支持按位布尔运算。|、&、~、^这些运算可以用到任何“整型”的数据类型上。void inplace_swap(int *x, int *y){
*y = *x ^ *y;
*x = *x ^ *y;
*y = *x ^ *y;
}
上面这段代码,就是利用了两个事实来实现*x和*y所指向的变量值进行了交换操作。两个事实分别是:

异或运算是可交换和可结合的;

对于任意的a, a ^ a = 0;

所以上述程序的计算过程如下:

初始:*x = a    *y = b
第一步:*x = a   *y = a ^ b
第二步:*x = a ^ (a ^ b) = (a ^ a) ^ b = b   *y = a ^ b
第三部:*x = b   *y = b ^ ( a ^ b) = (b ^ b) ^ a = a


但是注意,这种方式和通常的交换两个数值的技术不一样,当移动一个值时,我们不需要第三个位置来临时存放另外一个值。这种交换方式并没有性能上的优势,它仅仅是一个智力游戏!

位级运算常见的用法就是实现掩码运算:掩码是一个位模式,表示从一个字中选出的位的集合。

比如对于掩码0xFF(最低的8位都是1)表示一个字的低位字节。x&0xFF会得到一个由x的最低有效字节组成的值。 
 
 
【英文中几点钟的说法o'clock是什么的缩写?】

o'clock = of the clock.

在14世纪以前,人类还没有发明出来时钟,都是通过一些其他途径来获取和感知时间,比如日晷、沙漏等。直到 14 世纪,现代意义上的时钟雏形才得以发明。当时的时钟会自己报时“说出”:It's 7 of the clock!后来,随着时钟的普及和大众化,人们开始将 of 的 f 和 the 一带而过,简略地读成了 o'clock。
 https://ask.zkbhj.com/?/article/370 查看全部

【早读:《深入理解计算机系统》】

第二章 信息的表示和处理

表示代码
int sum(int x, int y) {
return x + y;
}
当我们在不同的机器上编译上面的程序得到的机器代码都不尽相同:


Linux32     55 89 e5 8b 45 0c 03 45 08 c9 c3
Windows  55 89 e5 8b 45 0c 03 45 08 5d c3
Sun           81 c3 e0 08 90 03 00 09



因此,二进制代码是不兼容的,无法在不同机器之间移植。

这得到一个计算机系统的基本概念:从机器的角度来看,程序仅仅只是字节序列,机器没有关于原始程序的任何信息。

布尔代数简介

计算机的核心都是围绕1和0来演化的。对于0和1的起源,要追溯到1850年前后乔治·布尔的工作,所以这个也叫布尔代数。即通过将逻辑值TRUE和FALSE编码为二进制1和0设计出的一种代数,以研究逻辑推理的基本原则。

20200806092721.jpg


上面分别列出了~(NOT)、&(AND)、|(OR)和^(EXCLUSIVE-OR)四种基本运算。

后来创立信息论领域的Claude Shannon首先建立了布尔代数和数字逻辑之间的联系。

将上述基础的布尔运算扩展到位向量运算。位向量就是固定长度为w、由0和1组成的串。

假设 w=4,a=[0110],b=[1100]。那么四种运算 a&b、a|b、a^b、~b 结果分别如下:

20200806092735.jpg

 
布尔运算&对|满足分配率:a&(b|c) = (a&b)|(a&c);反过来,|也满足对&的分配率,即:a|(b&c) = (a|b) & (a|c)。

位向量的一个有用应用就是表示有限集合,即用位向量来给集合进行编码。

C语言的一个有用特性就是支持按位布尔运算。|、&、~、^这些运算可以用到任何“整型”的数据类型上。
void inplace_swap(int *x, int *y){
*y = *x ^ *y;
*x = *x ^ *y;
*y = *x ^ *y;
}

上面这段代码,就是利用了两个事实来实现*x和*y所指向的变量值进行了交换操作。两个事实分别是:

异或运算是可交换和可结合的;

对于任意的a, a ^ a = 0;

所以上述程序的计算过程如下:


初始:*x = a    *y = b
第一步:*x = a   *y = a ^ b
第二步:*x = a ^ (a ^ b) = (a ^ a) ^ b = b   *y = a ^ b
第三部:*x = b   *y = b ^ ( a ^ b) = (b ^ b) ^ a = a



但是注意,这种方式和通常的交换两个数值的技术不一样,当移动一个值时,我们不需要第三个位置来临时存放另外一个值。这种交换方式并没有性能上的优势,它仅仅是一个智力游戏!

位级运算常见的用法就是实现掩码运算:掩码是一个位模式,表示从一个字中选出的位的集合。

比如对于掩码0xFF(最低的8位都是1)表示一个字的低位字节。x&0xFF会得到一个由x的最低有效字节组成的值。 
 
 
英文中几点钟的说法o'clock是什么的缩写?】


o'clock = of the clock.


在14世纪以前,人类还没有发明出来时钟,都是通过一些其他途径来获取和感知时间,比如日晷、沙漏等。直到 14 世纪,现代意义上的时钟雏形才得以发明。当时的时钟会自己报时“说出”:It's 7 of the clock!后来,随着时钟的普及和大众化,人们开始将 of 的 f 和 the 一带而过,简略地读成了 o'clock。
 https://ask.zkbhj.com/?/article/370

#每日精进#2020年8月5日

总结zkbhj 发表了文章 • 0 个评论 • 155 次浏览 • 2020-08-05 11:20 • 来自相关话题

【早读:《深入理解计算机系统》】

第二章 信息的表示和处理

寻址和字节顺序

在几乎所有机器上,多字节对象被存储为连续的字节序列,对象的地址为最小的地址。

排列表示一个对象的字节有两个通用的规则:

小端法:在内存中按照从最低有效字节到最高有效字节的顺序存储对象;
大端法:在内存中按照从最高有效字节到最低有效字节的顺序存储对象;一旦选择了特定的操作系统,那么字节顺序也就固定下来。Android和IOS只能够运行于小端模式下。

两种方式没有谁好谁坏之分,对于那种字节排序的选择都是任意的。
大小端的说法来自于Jonathan Swift的《格利弗游记》一书,其中交战的两个派别无法就打开一个半熟的鸡蛋应该从哪一端打开达成一致意见。
不同的端模式下,会有以下影响:
1、通过网络在不同端模式的机器间传递数据时,发送和接收时,需要转换网络标准;
2、在检查机器级程序时,对数据的解读方式;
3、在编写规避正常的类型系统的程序时。​#include <stdio.h>

typedef unsigned char *byte_pointer;
//typedef char *byte_pointer;

void show_bytes(byte_pointer start, size_t len) {
size_t i;
for (i = 0; i < len; i++)
printf("%p\t0x%.2x\n", &start, start);
printf("\n");
}

void show_int(int x) {
show_bytes((byte_pointer) &x, sizeof(int));
}

void show_float(float x) {
show_bytes((byte_pointer) &x, sizeof(float));
}

void show_pointer(void *x) {
show_bytes((byte_pointer) &x, sizeof(void *));
}

​在这段程序中,“byte_pointer start”告诉编译器,应该把这个指针看成指向一个字节序列,而不是指向一个原始数据类型的对象。然后,这个指针会被看成是对象使用的最低字节地址。
这种强制类型转换不会改变真实的指针,它们只是告诉编译器以新的数据类型来看待被指向的数据。
使用ASCII码作为字符码的任何系统上都是将得到相同的结果,与字节顺序和字节大小规则无关。因而,文本数据比二进制数据具有更强的平台独立性。
 
【HTML页面里怎么实现代码包含?】
https://ask.zkbhj.com/?/question/387
 
【Yii2框架中如何区分不同的场景指定赋值字段和检验规则】
 
场景(scenario)

分析上面问题,会发现关键点是批量赋值(massive assignment)和数据校验(validate)两个方法。如果对不同的场景指定赋值字段和检验规则,问题就迎刃而解。

Yii中的scenario有 安全属性 和 活跃属性 两个概念。安全属性用在批量赋值的load方法,只有安全属性才能被赋值;活跃属性用在规则校验的validate方法,在活跃属性集中并且定义了校验规则的属性才会被校验。活跃属性和安全属性的关系是,安全属性是活跃属性的子集。

\yii\base\Model类定义了默认场景:SCENARIO_DEFAULT(值为default)。默认场景下,出现在rules方法中的属性既是活跃属性,又是安全属性(这句话基本正确,看后续解释)。为不同场景指定活跃属性、安全属性以及校验器,可以通过覆盖senarios或rules两个方法实现(几乎每个Model类都会重写rules方法,senarios用得少)。

rules

先看rules方法。默认的属性加校验器定义方式,让每个属性既是安全属性,也是活跃属性。如果想让某个属性不是安全属性(不能通过load批量赋值),在属性名前加感叹号!即可。例如student中的user_id字段:public function rules()
{
return [
["!user_od", "required"],
["!user_id", "integer"],
["!user_od", "unique"],
// other rules
];
}user_id是活跃属性,在写入数据库时会被校验。但它不是安全属性,不能通过load方法进行赋值,解决了安全隐患。

再看rules方法按场景区分校验器规则的做法:定义校验器时on属性指定规则在哪些场景下生效,except属性则排除一些场景(如果不指定on和except,规则对所有场景生效)。例如:public function rules()
{
return [
["password", "string", "length" => [8, 16], "on" => ["signup"]], // 仅在signup场景时才被验证
["status", "integer", "except" => ["signup"], // 除了signup场景,其他情况都校验
// other rules
];
}在原来基础上新增感叹号和on/except属性,非常简便的就定义了非安全属性以及分场景指定校验规则。

scenarios

另外一种更清晰定义安全属性和活跃属性的做法是重写scenarios方法。scenarios方法返回一个数组,数组的键是场景名称,值是活跃属性集合(包饭安全属性)。例如student表的可能实现如下:public function scenarios()
{
return [
self::SCENARIO_DEFAULT => ["!user_id", "grade", "class", xxxx],
"update" => ["grade", "class", xxxx],
];
}默认情形下(学生报名),年级、班级这些信息是安全属性,但user_id不是,只能在程序内部赋值,并在插入数据时被校验;在修改信息时,user_id不是活跃属性,既不能被批量赋值,也不需要校验(事实上它不应该改变)。

scenarios方法只能定义活跃属性和安全属性,无法定义校验规则,需要和rules配合使用。
https://www.cnblogs.com/yangxunwu1992/p/6669380.html

 
【关于未来几年国内经济形势的分析总结】】
 
疫情发生以来,国内外经济形势发生了重大变化。国际局势也从全球化走向了逆全球化的道路,美国通过各种手段(贸易战、“中国病毒”论、打压华为、打压Tik Tok、关闭领事馆、干涉中国内政等)不断打压中国,各大企业也在不断将企业撤出中国。
 
最近最热门的经济词汇就是经济内循环,意思是说,以后要以国内市场为主,国内生产出来的东西,主要在自己国内消化掉,重新转化为生产力。也就是走内需拉动经济增长的路子。这是在疫情发生以及国内外形式发生如此变化之后的无奈之举。
 
我们过去拉动经济的三驾马车是外贸,基建和房地产,内需一向很薄弱,这几年内需份额有所提高,但还不足以成为拉动经济增长的主要动力。
 
最主要体现在居民收入不足,杠杆过高,中产被绑在房地产的战车上面等。
 
从微观层面看,中国消费结构呈现两边高中间低的M字型结构(健康的结构应该是相反的,两边低中间高,中产成为消费的主力)。所以中国目前的情况就是有钱人并不会受到高房价的影响而降低消费,反而还因此提高了消费能力,还在消费升级。然后相对经济收入低的人群消费能力本就低,地摊经济很火就说明这个问题。中产,由于购房等压力,从中端消费跌落到低端消费。
 
所以,国内要实现以经济内循环为主,国际循环为辅的国内国际双循环的新发展格局,有以下几种办法:
1、提高居民购买力
主要就是降房价和提高居民收入,提高居民收入很难,现在经济太差,没有需求就没有工作岗位,居民收入不下降已经很不错,但房价是有可能缓慢下降的

2、发展房地产。
过去十几年就是这么干的,但现在房价太高,已经到了伤害经济的底部,已经不能继续走拉抬房价发展经济的老路。

3、让股市走出慢牛。
目前看也相对可行,上层也一直在强调要通过资本市场服务实体经济,这也是我一向看好A股走牛的重要原因。
 
4、发展高科技和人民币国际化。
 
讲通俗一点,就是:

经济内循环其实就是要过苦日子的另一个代名词。也可以简单理解为好好干活,但不要想着能挣很多钱。在未来很长一段时间里面,物价会相对比较便宜。

 
https://zhuanlan.zhihu.com/p/165347415
 
【Go语言核心36讲:第12节 使用函数的正确姿势(1)】
 
在 Go 语言中,函数可是一等的(first-class)公民,函数类型也是一等的数据类型(函数类型属于引用类型,它的零值为nil)。
这意味着函数不但可以用于封装代码、分割功能、解耦逻辑,还可以化身为普通的值,在其他函数间传递、赋予变量、做类型判断和转换等。“函数是一等的公民”是函数式编程(functional programming)的重要特征。Go 语言在语言层面支持了函数式编程。

package main

import "fmt"

type Printer func(contents string) (n int, err error)

func printToStd(contents string) (bytesNum int, err error) {
return fmt.Println(contents)
}

func main() {
var p Printer
p = printToStd
p("something")
}函数的签名其实就是函数的参数列表和结果列表的统称,它定义了可用来鉴别不同函数的那些特征,同时也定义了我们与函数交互的方式。

各个参数和结果的名称不能算作函数签名的一部分,对于结果声明,名字也可以没有。且函数的名称也不算函数签名的一部分,只是个标识符而已。

高阶函数
 

1. 接受其他的函数作为参数传入;
2. 把其他的函数作为结果返回。

只要满足了其中任意一个特点,我们就可以说这个函数是一个高阶函数。
 
卫述语句
 
卫述语句是指被用来检查关键的先决条件的合法性,并在检查未通过的情况下立即终止当前代码块执行的语句。在 Go 语言中,if 语句常被作为卫述语句。
if op == nil {
return 0, errors.New("invalid operation")
} 查看全部
【早读:《深入理解计算机系统》】

第二章 信息的表示和处理

寻址和字节顺序

在几乎所有机器上,多字节对象被存储为连续的字节序列,对象的地址为最小的地址。

排列表示一个对象的字节有两个通用的规则:


小端法:在内存中按照从最低有效字节到最高有效字节的顺序存储对象;
大端法:在内存中按照从最高有效字节到最低有效字节的顺序存储对象;一旦选择了特定的操作系统,那么字节顺序也就固定下来。Android和IOS只能够运行于小端模式下。


两种方式没有谁好谁坏之分,对于那种字节排序的选择都是任意的。
大小端的说法来自于Jonathan Swift的《格利弗游记》一书,其中交战的两个派别无法就打开一个半熟的鸡蛋应该从哪一端打开达成一致意见。
不同的端模式下,会有以下影响:
1、通过网络在不同端模式的机器间传递数据时,发送和接收时,需要转换网络标准;
2、在检查机器级程序时,对数据的解读方式;
3、在编写规避正常的类型系统的程序时。
​#include <stdio.h>

typedef unsigned char *byte_pointer;
//typedef char *byte_pointer;

void show_bytes(byte_pointer start, size_t len) {
size_t i;
for (i = 0; i < len; i++)
printf("%p\t0x%.2x\n", &start, start);
printf("\n");
}

void show_int(int x) {
show_bytes((byte_pointer) &x, sizeof(int));
}

void show_float(float x) {
show_bytes((byte_pointer) &x, sizeof(float));
}

void show_pointer(void *x) {
show_bytes((byte_pointer) &x, sizeof(void *));
}

​在这段程序中,“byte_pointer start”告诉编译器,应该把这个指针看成指向一个字节序列,而不是指向一个原始数据类型的对象。然后,这个指针会被看成是对象使用的最低字节地址。
这种强制类型转换不会改变真实的指针,它们只是告诉编译器以新的数据类型来看待被指向的数据。
使用ASCII码作为字符码的任何系统上都是将得到相同的结果,与字节顺序和字节大小规则无关。因而,文本数据比二进制数据具有更强的平台独立性。
 
【HTML页面里怎么实现代码包含?】
https://ask.zkbhj.com/?/question/387
 
【Yii2框架中如何区分不同的场景指定赋值字段和检验规则】
 
场景(scenario)

分析上面问题,会发现关键点是批量赋值(massive assignment)和数据校验(validate)两个方法。如果对不同的场景指定赋值字段和检验规则,问题就迎刃而解。

Yii中的scenario有 安全属性 和 活跃属性 两个概念。安全属性用在批量赋值的load方法,只有安全属性才能被赋值;活跃属性用在规则校验的validate方法,在活跃属性集中并且定义了校验规则的属性才会被校验。活跃属性和安全属性的关系是,安全属性是活跃属性的子集。

\yii\base\Model类定义了默认场景:SCENARIO_DEFAULT(值为default)。默认场景下,出现在rules方法中的属性既是活跃属性,又是安全属性(这句话基本正确,看后续解释)。为不同场景指定活跃属性、安全属性以及校验器,可以通过覆盖senarios或rules两个方法实现(几乎每个Model类都会重写rules方法,senarios用得少)。

rules

先看rules方法。默认的属性加校验器定义方式,让每个属性既是安全属性,也是活跃属性。如果想让某个属性不是安全属性(不能通过load批量赋值),在属性名前加感叹号!即可。例如student中的user_id字段:
public function rules()
{
return [
["!user_od", "required"],
["!user_id", "integer"],
["!user_od", "unique"],
// other rules
];
}
user_id是活跃属性,在写入数据库时会被校验。但它不是安全属性,不能通过load方法进行赋值,解决了安全隐患。

再看rules方法按场景区分校验器规则的做法:定义校验器时on属性指定规则在哪些场景下生效,except属性则排除一些场景(如果不指定on和except,规则对所有场景生效)。例如:
public function rules()
{
return [
["password", "string", "length" => [8, 16], "on" => ["signup"]], // 仅在signup场景时才被验证
["status", "integer", "except" => ["signup"], // 除了signup场景,其他情况都校验
// other rules
];
}
在原来基础上新增感叹号和on/except属性,非常简便的就定义了非安全属性以及分场景指定校验规则。

scenarios

另外一种更清晰定义安全属性和活跃属性的做法是重写scenarios方法。scenarios方法返回一个数组,数组的键是场景名称,值是活跃属性集合(包饭安全属性)。例如student表的可能实现如下:
public function scenarios()
{
return [
self::SCENARIO_DEFAULT => ["!user_id", "grade", "class", xxxx],
"update" => ["grade", "class", xxxx],
];
}
默认情形下(学生报名),年级、班级这些信息是安全属性,但user_id不是,只能在程序内部赋值,并在插入数据时被校验;在修改信息时,user_id不是活跃属性,既不能被批量赋值,也不需要校验(事实上它不应该改变)。

scenarios方法只能定义活跃属性和安全属性,无法定义校验规则,需要和rules配合使用。
https://www.cnblogs.com/yangxunwu1992/p/6669380.html

 
【关于未来几年国内经济形势的分析总结】】
 
疫情发生以来,国内外经济形势发生了重大变化。国际局势也从全球化走向了逆全球化的道路,美国通过各种手段(贸易战、“中国病毒”论、打压华为、打压Tik Tok、关闭领事馆、干涉中国内政等)不断打压中国,各大企业也在不断将企业撤出中国。
 
最近最热门的经济词汇就是经济内循环,意思是说,以后要以国内市场为主,国内生产出来的东西,主要在自己国内消化掉,重新转化为生产力。也就是走内需拉动经济增长的路子。这是在疫情发生以及国内外形式发生如此变化之后的无奈之举。
 
我们过去拉动经济的三驾马车是外贸,基建和房地产,内需一向很薄弱,这几年内需份额有所提高,但还不足以成为拉动经济增长的主要动力。
 
最主要体现在居民收入不足杠杆过高中产被绑在房地产的战车上面等。
 
从微观层面看,中国消费结构呈现两边高中间低的M字型结构(健康的结构应该是相反的,两边低中间高,中产成为消费的主力)。所以中国目前的情况就是有钱人并不会受到高房价的影响而降低消费,反而还因此提高了消费能力,还在消费升级。然后相对经济收入低的人群消费能力本就低,地摊经济很火就说明这个问题。中产,由于购房等压力,从中端消费跌落到低端消费。
 
所以,国内要实现以经济内循环为主,国际循环为辅的国内国际双循环的新发展格局,有以下几种办法:
1、提高居民购买力
主要就是降房价和提高居民收入,提高居民收入很难,现在经济太差,没有需求就没有工作岗位,居民收入不下降已经很不错,但房价是有可能缓慢下降的

2、发展房地产。
过去十几年就是这么干的,但现在房价太高,已经到了伤害经济的底部,已经不能继续走拉抬房价发展经济的老路。

3、让股市走出慢牛。
目前看也相对可行,上层也一直在强调要通过资本市场服务实体经济,这也是我一向看好A股走牛的重要原因。
 
4、发展高科技和人民币国际化。
 
讲通俗一点,就是:


经济内循环其实就是要过苦日子的另一个代名词。也可以简单理解为好好干活,但不要想着能挣很多钱。在未来很长一段时间里面,物价会相对比较便宜。


 
https://zhuanlan.zhihu.com/p/165347415
 
【Go语言核心36讲:第12节 使用函数的正确姿势(1)】
 
在 Go 语言中,函数可是一等的(first-class)公民,函数类型也是一等的数据类型(函数类型属于引用类型,它的零值为nil)。
这意味着函数不但可以用于封装代码、分割功能、解耦逻辑,还可以化身为普通的值,在其他函数间传递、赋予变量、做类型判断和转换等。“函数是一等的公民”是函数式编程(functional programming)的重要特征。Go 语言在语言层面支持了函数式编程。

package main

import "fmt"

type Printer func(contents string) (n int, err error)

func printToStd(contents string) (bytesNum int, err error) {
return fmt.Println(contents)
}

func main() {
var p Printer
p = printToStd
p("something")
}
函数的签名其实就是函数的参数列表和结果列表的统称,它定义了可用来鉴别不同函数的那些特征,同时也定义了我们与函数交互的方式。


各个参数和结果的名称不能算作函数签名的一部分,对于结果声明,名字也可以没有。且函数的名称也不算函数签名的一部分,只是个标识符而已。


高阶函数
 


1. 接受其他的函数作为参数传入;
2. 把其他的函数作为结果返回。


只要满足了其中任意一个特点,我们就可以说这个函数是一个高阶函数。
 
卫述语句
 
卫述语句是指被用来检查关键的先决条件的合法性,并在检查未通过的情况下立即终止当前代码块执行的语句。在 Go 语言中,if 语句常被作为卫述语句。
if op == nil { 
return 0, errors.New("invalid operation")
}

#每日精进#2020年8月4日

总结zkbhj 发表了文章 • 0 个评论 • 142 次浏览 • 2020-08-04 14:51 • 来自相关话题

【早读:《深入理解计算机系统》】
 
第二章 信息的表示和处理

大多数计算机使用8位的块,或字节,作为最小的可寻址内存单位。

机器级程序将内存视为一个非常大的字节数组,称为虚拟内存。

内存中每个字节由一个唯一的数字来标识,称为它的地址。所有可能的地址的集合就称为虚拟地址空间。

所以,这个虚拟地址空间只是一个展现给机器级程序的概念性映像。实际上,它将DRAM、闪存、磁盘存储器等和操作系统软件结合起来,封装了复杂性,为程序提供一个看上去统一的字节数组。


C语言中的一个指针的值,都是某个存储块的第一个字节的虚拟地址。每个程序对象可以简单地视为一个字节块,而程序本身就是一个字节序列。

十六进制表示法

由于二进制和十进制对于描述位模式来说都非常不方便:二进制太冗长,十进制和位模式的互相转化很麻烦,替代的方法就是引入16进制。

以0x或者0X开头,0~9、A~F,不区分大小写且大小写不敏感。

重要的是二进制、十进制和十六进制之间的互相转换方法,详细的可以进入凯冰科技知识共享中心搜索相关问题或文章查看。

对于x=2的n次方这个公式,转换十六进制,可以转化为n=i+4j,然后得到的十六进制就是:0x + 2的i次方 + j个0,比如512,是2的9次方,9=1+4*2,所以十六进制就是0x200。

字数据大小

每台计算机都有一个字长,指明指针数据的标称大小。字长决定虚拟地址空间的最大大小。32位字长限制的虚拟地址空间位4千兆字节(约4GB),而现在比较普及的64位字长的虚拟空间位16EB。

大多数64位机器也可以运行32位机器编译的程序,这是一种向后兼容。//该编译后的程序可以在32或64位机器上运行
linux> gcc -m32 prog.c

//该编译后,只能在64位机器上运行
linux> gcc -m64 prog.c我们将程序称为32位程序或64位程序,区别在于该程序是如何编译的,而不是其运行的机器类型。

ISO C99引入了确定大小的数据类型,int32_t 和int64_t,其数据大小是固定的,分别为4个字节和8个字节。使用确切大小的整数类型是程序员准确控制数据表示的最佳途径。

程序员应该力图使他们的程序可以在不同的机器和编译器上可移植,可移植的一方面就是说程序对不同数据类型的确切大小不敏感。

比如许多程序员假设一个声明为int类型的程序对象能被用来存储一个指针,这在大多数32位的机器上能够正常工作,但是在一台64位的机器上却会导致问题。所以,1980到2010年期间(32位机器是主流)编写的程序,之后64位机器陆续普及之后,迁移过来的程序就暴露出来许多隐藏的对字长的依赖性问题,导致错误。

【垂直行业如电商如何衡量搜索引擎的优劣】

在电商行业中,无论是2B还是2C,最终的业务目的就是交易成单,众所周知搜索服务旨在让消费者能够更快的定位到自己想要的产品。

一般电商搜索的核心是搜索精度和搜索广度,精度就是搜索的精确性,广度就是搜索结果的范围,其关键结果肯定是“为用户找到想要的商品”,但过于追求搜索的精确度就会导致出现搜索的结果比较少或结果为0的情况,用户搜不到商品势必会引发流失,因此在搜索服务里面还可以做的就是给用户提供一些相关性搜索结果。那么搜索做的好不好,其实就是在搜索精度和搜索广度二者之间做一个比较好的平衡点。

搜索过程中遇到的问题:

1.随机性发现的Bad case
2.KPI或者OKR考核
3.业务方诉求

核心指标
 
“搜索PV”:指访问搜索页面的次数;“搜索UV”:访问过搜索结果页的用户数;“无结果率”:空结果PV/搜索PV,无结果率越低,代表客户搜索需求解决情况越好;“TOP5 PV—CTR”:指该query search结果中,排在前五位的item有被点击的搜索PV/该query搜索PV该指标能一定程度反应排序效果;“人均搜索PV”:搜索PV/搜索UV;该指标的含义比较复杂,一方面人均pv大的话可能代表用户对搜索比较感兴趣,但另一方面人均pv大也可能代表搜索召回的结果较差,导致用户无法使用较少的点击找到满足需求的结果;“有点击搜索PV占比”:有点击搜索PV/搜索PV数;“PV-CTR”:搜索结果页item点击数/搜索PV数;“UV-CTR”:点击的uv / 曝光的uv;“Item-CTR”:搜索结果页item点击数/搜索结果页item总曝光PV数;
 
搜索技术等级分类





 
https://developer.aliyun.com/article/769492
 
  查看全部

【早读:《深入理解计算机系统》】
 
第二章 信息的表示和处理

大多数计算机使用8位的块,或字节,作为最小的可寻址内存单位。

机器级程序将内存视为一个非常大的字节数组,称为虚拟内存。

内存中每个字节由一个唯一的数字来标识,称为它的地址。所有可能的地址的集合就称为虚拟地址空间。

所以,这个虚拟地址空间只是一个展现给机器级程序的概念性映像。实际上,它将DRAM、闪存、磁盘存储器等和操作系统软件结合起来,封装了复杂性,为程序提供一个看上去统一的字节数组。


C语言中的一个指针的值,都是某个存储块的第一个字节的虚拟地址。每个程序对象可以简单地视为一个字节块,而程序本身就是一个字节序列。

十六进制表示法

由于二进制和十进制对于描述位模式来说都非常不方便:二进制太冗长,十进制和位模式的互相转化很麻烦,替代的方法就是引入16进制。

以0x或者0X开头,0~9、A~F,不区分大小写且大小写不敏感。

重要的是二进制、十进制和十六进制之间的互相转换方法,详细的可以进入凯冰科技知识共享中心搜索相关问题或文章查看。

对于x=2的n次方这个公式,转换十六进制,可以转化为n=i+4j,然后得到的十六进制就是:0x + 2的i次方 + j个0,比如512,是2的9次方,9=1+4*2,所以十六进制就是0x200。

字数据大小

每台计算机都有一个字长,指明指针数据的标称大小。字长决定虚拟地址空间的最大大小。32位字长限制的虚拟地址空间位4千兆字节(约4GB),而现在比较普及的64位字长的虚拟空间位16EB。

大多数64位机器也可以运行32位机器编译的程序,这是一种向后兼容。
//该编译后的程序可以在32或64位机器上运行
linux> gcc -m32 prog.c

//该编译后,只能在64位机器上运行
linux> gcc -m64 prog.c
我们将程序称为32位程序或64位程序,区别在于该程序是如何编译的,而不是其运行的机器类型。

ISO C99引入了确定大小的数据类型,int32_t 和int64_t,其数据大小是固定的,分别为4个字节和8个字节。使用确切大小的整数类型是程序员准确控制数据表示的最佳途径。

程序员应该力图使他们的程序可以在不同的机器和编译器上可移植,可移植的一方面就是说程序对不同数据类型的确切大小不敏感。

比如许多程序员假设一个声明为int类型的程序对象能被用来存储一个指针,这在大多数32位的机器上能够正常工作,但是在一台64位的机器上却会导致问题。所以,1980到2010年期间(32位机器是主流)编写的程序,之后64位机器陆续普及之后,迁移过来的程序就暴露出来许多隐藏的对字长的依赖性问题,导致错误。

【垂直行业如电商如何衡量搜索引擎的优劣】

在电商行业中,无论是2B还是2C,最终的业务目的就是交易成单,众所周知搜索服务旨在让消费者能够更快的定位到自己想要的产品。

一般电商搜索的核心是搜索精度和搜索广度,精度就是搜索的精确性,广度就是搜索结果的范围,其关键结果肯定是“为用户找到想要的商品”,但过于追求搜索的精确度就会导致出现搜索的结果比较少或结果为0的情况,用户搜不到商品势必会引发流失,因此在搜索服务里面还可以做的就是给用户提供一些相关性搜索结果。那么搜索做的好不好,其实就是在搜索精度和搜索广度二者之间做一个比较好的平衡点。

搜索过程中遇到的问题:


1.随机性发现的Bad case
2.KPI或者OKR考核
3.业务方诉求


核心指标
 
  • “搜索PV”:指访问搜索页面的次数;
  • “搜索UV”:访问过搜索结果页的用户数;
  • “无结果率”:空结果PV/搜索PV,无结果率越低,代表客户搜索需求解决情况越好;
  • “TOP5 PV—CTR”:指该query search结果中,排在前五位的item有被点击的搜索PV/该query搜索PV该指标能一定程度反应排序效果;
  • “人均搜索PV”:搜索PV/搜索UV;该指标的含义比较复杂,一方面人均pv大的话可能代表用户对搜索比较感兴趣,但另一方面人均pv大也可能代表搜索召回的结果较差,导致用户无法使用较少的点击找到满足需求的结果;
  • “有点击搜索PV占比”:有点击搜索PV/搜索PV数;
  • “PV-CTR”:搜索结果页item点击数/搜索PV数;
  • “UV-CTR”:点击的uv / 曝光的uv;
  • “Item-CTR”:搜索结果页item点击数/搜索结果页item总曝光PV数;

 
搜索技术等级分类

13cfe86b5b2f4aacba814f4fb2f080e5.png

 
https://developer.aliyun.com/article/769492
 
 

使用了Go mod管理的项目里如何添加新的第三方包?

回复

GoLangzkbhj 回复了问题 • 1 人关注 • 1 个回复 • 175 次浏览 • 2020-09-17 11:32 • 来自相关话题

MySQL如何通过sql查询数据的区间分布情况?

回复

数据库zkbhj 回复了问题 • 1 人关注 • 1 个回复 • 165 次浏览 • 2020-09-07 10:42 • 来自相关话题

如何给页面增加文字水印?

回复

前端开发zkbhj 回复了问题 • 1 人关注 • 1 个回复 • 300 次浏览 • 2020-08-06 14:31 • 来自相关话题

怎么理解软件开发中的上游和下游?

回复

专业名词zkbhj 回复了问题 • 1 人关注 • 1 个回复 • 916 次浏览 • 2020-07-22 09:35 • 来自相关话题

如何用守护进程启动Beego应用?

回复

GoLangzkbhj 回复了问题 • 1 人关注 • 1 个回复 • 395 次浏览 • 2020-07-09 20:04 • 来自相关话题

生产者消费者模型是什么?

回复

GoLangzkbhj 回复了问题 • 1 人关注 • 1 个回复 • 441 次浏览 • 2020-06-19 17:26 • 来自相关话题

一段程序,从生成可执行文件,到执行完成,数据在计算机内是怎么存储和分配的?

回复

常识zkbhj 回复了问题 • 1 人关注 • 1 个回复 • 370 次浏览 • 2020-06-09 17:34 • 来自相关话题

#2020学习打卡##Go语言高级编程# 什么是第一类对象?

回复

GoLangzkbhj 回复了问题 • 1 人关注 • 1 个回复 • 325 次浏览 • 2020-06-09 17:23 • 来自相关话题

#2020学习打卡##Go语言高级编程# Go语言中的Nil到底是什么?

回复

GoLangzkbhj 回复了问题 • 1 人关注 • 1 个回复 • 297 次浏览 • 2020-06-08 17:26 • 来自相关话题

#2020学习打卡##Go语言高级编程# 什么是鸭子对象模型?

回复

GoLangzkbhj 回复了问题 • 1 人关注 • 1 个回复 • 310 次浏览 • 2020-06-08 16:01 • 来自相关话题

LeetCode刷题Day2:两数相加

LeetCodezkbhj 发表了文章 • 0 个评论 • 65 次浏览 • 2020-09-22 20:57 • 来自相关话题

两数相加

给出两个 非空 的链表用来表示两个非负的整数。其中,它们各自的位数是按照 逆序 的方式存储的,并且它们的每个节点只能存储 一位 数字。如果,我们将这两个数相加起来,则会返回一个新的链表来表示它们的和。 

您可以假设除了数字 0 之外,这两个数都不会以 0 开头。

示例:

输入:(2 -> 4 -> 3) + (5 -> 6 -> 4) 
输出:7 -> 0 -> 8
原因:342 + 465 = 807LeetCode

 首先发一下我的思路已经给出的最初答案(大部分情况是正确的,但是超出数字范围就会和预期结果不一致),总之先把分析思路分享出来。


思路一:常规思路

分别循环两个链表,因为高位是个位,所以不断乘以10的n次方,n代表循环次数,从0开始,直到循环结束,就得到了两个无符号整数。所以,这就要求对输入数字有范围限制的要求,否则数字太大,超出整数的最大范围,得到的结果会因为发生溢出而不符合预期。

然后将两个整数进行加法运算,得到最终结果对应的int数字。

然后再将这个整数转化成字符串,循环字符串,得到字符对应的ASCII码点,然后减去48即可“间接“得到对应的数字。并按序生成链表。
 
func myPow(a,n int) int {
result := int(1)
for i:= n; i >0 ; i >>= 1 {
if i&1 != 0 {
result *=a
}
a *=a
}
return result
}

func addTwoNumbers(l1 *ListNode, l2 *ListNode) *ListNode {
var a,b,c,i,j int
p1 := l1
for p1!= nil {
a = a + p1.Val * myPow(10,i)
i++
p1 = p1.Next
}
p2 := l2
for p2!= nil {
b = b + p2.Val * myPow(10,j)
j++
p2 = p2.Next
}
c = a + b

str := strconv.Itoa(c)
headNode := new(ListNode)
current := headNode
current.Next = nil
for _,one := range str {
preNode := current
current.Val = int(one-48)
current = new(ListNode)
current.Next = preNode

}

return current.Next
}
思路一:高级思路(学习)
对应位置的数字相加,跟10取模,即可得到该位置上十进制数相加之后的数字,然后和10进行整除,如果两个数相加小于10,则整除之后是0,否则是1,即要进位的1,所以直到两个链表都循环完,或者最后没有进位时,停止循环,最终返回。
func addTwoNumbers(l1 *ListNode, l2 *ListNode) *ListNode {
headList := new(ListNode)
head := headList
num := 0

for (l1 != nil || l2 != nil || num > 0) {
headList.Next = new(ListNode)
headList = headList.Next
if l1 != nil {
num = num + l1.Val
l1 = l1.Next
}
if l2 != nil {
num = num + l2.Val
l2 = l2.Next
}
headList.Val = (num) % 10
num = num / 10
}

return head.Next
}
  查看全部


两数相加

给出两个 非空 的链表用来表示两个非负的整数。其中,它们各自的位数是按照 逆序 的方式存储的,并且它们的每个节点只能存储 一位 数字。如果,我们将这两个数相加起来,则会返回一个新的链表来表示它们的和。 

您可以假设除了数字 0 之外,这两个数都不会以 0 开头。

示例:

输入:(2 -> 4 -> 3) + (5 -> 6 -> 4) 
输出:7 -> 0 -> 8
原因:342 + 465 = 807LeetCode


 首先发一下我的思路已经给出的最初答案(大部分情况是正确的,但是超出数字范围就会和预期结果不一致),总之先把分析思路分享出来。


思路一:常规思路

分别循环两个链表,因为高位是个位,所以不断乘以10的n次方,n代表循环次数,从0开始,直到循环结束,就得到了两个无符号整数。所以,这就要求对输入数字有范围限制的要求,否则数字太大,超出整数的最大范围,得到的结果会因为发生溢出而不符合预期。

然后将两个整数进行加法运算,得到最终结果对应的int数字。

然后再将这个整数转化成字符串,循环字符串,得到字符对应的ASCII码点,然后减去48即可“间接“得到对应的数字。并按序生成链表。
 
func myPow(a,n int) int {
result := int(1)
for i:= n; i >0 ; i >>= 1 {
if i&1 != 0 {
result *=a
}
a *=a
}
return result
}

func addTwoNumbers(l1 *ListNode, l2 *ListNode) *ListNode {
var a,b,c,i,j int
p1 := l1
for p1!= nil {
a = a + p1.Val * myPow(10,i)
i++
p1 = p1.Next
}
p2 := l2
for p2!= nil {
b = b + p2.Val * myPow(10,j)
j++
p2 = p2.Next
}
c = a + b

str := strconv.Itoa(c)
headNode := new(ListNode)
current := headNode
current.Next = nil
for _,one := range str {
preNode := current
current.Val = int(one-48)
current = new(ListNode)
current.Next = preNode

}

return current.Next
}

思路一:高级思路(学习)
对应位置的数字相加,跟10取模,即可得到该位置上十进制数相加之后的数字,然后和10进行整除,如果两个数相加小于10,则整除之后是0,否则是1,即要进位的1,所以直到两个链表都循环完,或者最后没有进位时,停止循环,最终返回。

func addTwoNumbers(l1 *ListNode, l2 *ListNode) *ListNode {
headList := new(ListNode)
head := headList
num := 0

for (l1 != nil || l2 != nil || num > 0) {
headList.Next = new(ListNode)
headList = headList.Next
if l1 != nil {
num = num + l1.Val
l1 = l1.Next
}
if l2 != nil {
num = num + l2.Val
l2 = l2.Next
}
headList.Val = (num) % 10
num = num / 10
}

return head.Next
}

 

LeetCode刷题Day1:两数之和

LeetCodezkbhj 发表了文章 • 0 个评论 • 60 次浏览 • 2020-09-22 20:55 • 来自相关话题

从今天开始,把LeetCode刷题作为每天日常计划的一项。尽早做准备,同时也每天对算法和相关的技能知识通过刷题的形式进行潜移默化的提升和加深理解。其实身边已经有很多程序猿朋友都在LeetCode刷题了,再不参与进来,都要出圈儿了呀。

今天是Day1,从第一道题目开始:
 

两数之和

给定一个整数数组 nums 和一个目标值 target,请你在该数组中找出和为目标值的那 两个 整数,并返回他们的数组下标。你可以假设每种输入只会对应一个答案。但是,数组中同一个元素不能使用两遍。 

示例: 

给定 nums = [2, 7, 11, 15], target = 9 

因为 nums[0] + nums[1] = 2 + 7 = 9 所以返回 [0, 1]LeetCode
 

 
思路一:暴力遍历

也是最简单最原始的未经优化的方案思路。

首先明确题目要求,可以假设每种输入只会对应一个答案,即一定会有且只有一对符合要求的数据。

所以得到数组的长度,然后从第一个开始进行循环,将目标值target减去当前被循环的值,得到与之可以配对的值,然后在数组中进行寻找,如果找到,就返回2个符合要求的值的索引,否则继续下一次寻找,直到找到。

最外层循环的条件中 x&y异或之后如果为1,说明已经找到对应的结果,就不再循环。时间复杂度是O(n²),空间复杂度是O(1)。
func twoSum(nums []int, target int) []int {
len := len(nums)
var x,y int

for i:=0; i < len && x|y == 0; i++ {
findNum := target - nums[i]
for j:=0; j < len; j++ {
if findNum == nums[j] {
x = i;
y = j;
}
}

}

return []int{x,y}
}
思路二:进一步提升效率,可以利用哈希表
从第一个思路中,可以看到其实在判断一个值在一个“表”中是否存在的时候,我们采用了遍历的方式,这样的话时间复杂度就是O(n),但是大家肯定都知道,哈希表是最擅长做这个事情的。它可以把时间复杂度降到O(1)。所以,这样思路就很清晰了,我们需要先把传过来的数组生成对应的哈希结构,也就是Go里面的map字典,这样我们就可以快速的找到某值在还是不在了。我们把数组的value作为map的key,index作为map的value。代码实现如下。
 
func twoSum(nums []int, target int) []int {
var hashMap map[int]int
len := len(nums)
var x,y,found int

//初始化字典
hashMap = make(map[int]int)
for i:=0; i < len; i++ {
hashMap[nums[i]] = i
}

for j:=0; j < len && found == 0; j++ {
findNum := target - nums[j]
v, ok := hashMap[findNum]
if ok && v != j {
found = 1
x = j
y = v
}

}

return []int{x,y}
}
今天这道题的难度级别是简单,但是带我回顾了Go语言的数组、map的相关知识和用法,以及哈希表这一知识点。 查看全部
从今天开始,把LeetCode刷题作为每天日常计划的一项。尽早做准备,同时也每天对算法和相关的技能知识通过刷题的形式进行潜移默化的提升和加深理解。其实身边已经有很多程序猿朋友都在LeetCode刷题了,再不参与进来,都要出圈儿了呀。

今天是Day1,从第一道题目开始:
 


两数之和

给定一个整数数组 nums 和一个目标值 target,请你在该数组中找出和为目标值的那 两个 整数,并返回他们的数组下标。你可以假设每种输入只会对应一个答案。但是,数组中同一个元素不能使用两遍。 

示例: 

给定 nums = [2, 7, 11, 15], target = 9 

因为 nums[0] + nums[1] = 2 + 7 = 9 所以返回 [0, 1]LeetCode
 


 
思路一:暴力遍历

也是最简单最原始的未经优化的方案思路。

首先明确题目要求,可以假设每种输入只会对应一个答案,即一定会有且只有一对符合要求的数据。

所以得到数组的长度,然后从第一个开始进行循环,将目标值target减去当前被循环的值,得到与之可以配对的值,然后在数组中进行寻找,如果找到,就返回2个符合要求的值的索引,否则继续下一次寻找,直到找到。

最外层循环的条件中 x&y异或之后如果为1,说明已经找到对应的结果,就不再循环。时间复杂度是O(n²),空间复杂度是O(1)。

func twoSum(nums []int, target int) []int {
len := len(nums)
var x,y int

for i:=0; i < len && x|y == 0; i++ {
findNum := target - nums[i]
for j:=0; j < len; j++ {
if findNum == nums[j] {
x = i;
y = j;
}
}

}

return []int{x,y}
}

思路二:进一步提升效率,可以利用哈希表
从第一个思路中,可以看到其实在判断一个值在一个“表”中是否存在的时候,我们采用了遍历的方式,这样的话时间复杂度就是O(n),但是大家肯定都知道,哈希表是最擅长做这个事情的。它可以把时间复杂度降到O(1)。所以,这样思路就很清晰了,我们需要先把传过来的数组生成对应的哈希结构,也就是Go里面的map字典,这样我们就可以快速的找到某值在还是不在了。我们把数组的value作为map的key,index作为map的value。代码实现如下。
 

func twoSum(nums []int, target int) []int {
var hashMap map[int]int
len := len(nums)
var x,y,found int

//初始化字典
hashMap = make(map[int]int)
for i:=0; i < len; i++ {
hashMap[nums[i]] = i
}

for j:=0; j < len && found == 0; j++ {
findNum := target - nums[j]
v, ok := hashMap[findNum]
if ok && v != j {
found = 1
x = j
y = v
}

}

return []int{x,y}
}

今天这道题的难度级别是简单,但是带我回顾了Go语言的数组、map的相关知识和用法,以及哈希表这一知识点。

如何快速提升Go程序性能?

GoLangzkbhj 发表了文章 • 0 个评论 • 90 次浏览 • 2020-09-15 18:39 • 来自相关话题

这篇文章中列出了一些不需要太多精力就能显著提高性能的技巧,并不包含那些需要太多精力或需要大幅度修改程序结构的技巧。
 
开始优化之前
 
开始优化之前,首先应该花些时间找出一个合适的基准线,以便稍后比较。如果没有基准,那就等于摸着石头过河,根本不知道自己的优化有没有效果。首先要编写性能测试程序,然后生成能用于pprof的profile文件。最好可以编写Go的性能测试脚本(https://dave.cheney.net/2013/0 ... in-go),这样可以很容易地使用pprof,还可以评测内存分配情况。还可以使用benchcmp,这个工具可以帮助比较两次性能测试之间的性能差异。

如果代码很难做性能测试,那就从你能测量时间的部分开始。可以利用runtime/pprof手工测量代码。

现在开始吧!
 
使用sync.Pool重用之前分配过的对象
 
sync.Pool实现了一个空闲列表(free-list)。这样可以重新使用之前分配过的对象。这样做可以将对象分配的代价平摊到多次使用上,减少垃圾回收器的工作。API非常简单:只需实现一个函数,用来分配新的对象即可。它会返回指针类型。
 
var bufpool = sync.Pool{
New: func() interface{} {
buf := make([]byte, 512)
return &buf
}}之后,可以用Get()从池中获取对象,用完之后用Put()将对象放回。

不过要注意一些陷阱。在Go 1.13之前,每次发生垃圾回收时该池都会被清空。对于需要分配大量对象的程序来说,这可能会造成性能的影响。在1.13版本中似乎GC后能保留更多对象了(https://go-review.googlesource.com/c/go/+/162919/)。

你可能需要在将对象放回池中之前将其结构的字段清空。

如果不这样做,就可能从池中获得一个“脏”的对象,它包含之前使用过的数据。这可能会造成严重的安全问题!
 
安全的做法就是明确清空内存:// reset resets all fields of the AuthenticationResponse before pooling it.
func (a* AuthenticationResponse) reset() {
a.Token = ""
a.UserID = ""
}

rsp := authPool.Get().(*AuthenticationResponse)
defer func() {
rsp.reset()
authPool.Put(rsp)
}()
在大map中避免使用包含指针的结构作为map的键
 
关于Go中大型堆的性能问题已经有很多人讨论过了。在垃圾回收过程中,运行时会扫描包含指针的对象并遍历其指针。如果你有非常大的map[string]int,那么垃圾回收器就不得不在每次垃圾回收过程中检查map中的每个字符串,因为字符串包含指针。

这个例子中我们向一个map[string]int中写入了一千万个元素,然后测量垃圾回收的时间。map是在包的作用域中分配的,以保证它被分配到堆上。
import (
"fmt"
"runtime"
"strconv"
"time"
)

const (
numElements = 10000000
)

var foo = map[string]int{}

func timeGC() {
t := time.Now()
runtime.GC()
fmt.Printf("gc took: %s\n", time.Since(t))
}

func main() {
for i := 0; i < numElements; i++ {
foo[strconv.Itoa(i)] = i
}

for {
timeGC()
time.Sleep(1 * time.Second)
}
}运行后可以得到以下结果:gc took: 98.726321ms
gc took: 105.524633ms
gc took: 102.829451ms
gc took: 102.71908ms
gc took: 103.084104ms
gc took: 104.821989ms对于计算机来说这花得时间太多了!

怎样可以改进呢?最好是能尽量去掉指针,这样能减少垃圾回收器需要遍历的指针数量。由于字符串包含指针,因此我们可以用map[int]int来实现:
 
import (
"fmt"
"runtime"
"time"
)

const (
numElements = 10000000
)

var foo = map[int]int{}

func timeGC() {
t := time.Now()
runtime.GC()
fmt.Printf("gc took: %s\n", time.Since(t))
}

func main() {
for i := 0; i < numElements; i++ {
foo[i] = i
}

for {
timeGC()
time.Sleep(1 * time.Second)
}
}重新运行程序,结果如下:
 
gc took: 3.608993ms
gc took: 3.926913ms
gc took: 3.955706ms
gc took: 4.063795ms
gc took: 3.91519ms
gc took: 3.75226ms好多了。垃圾回收的时间减少了97%。在生产环境下,字符串需要进行hash之后再插入到map中。
 
生成marshalling代码以避免运行时反射
 
将数据结构marshalh或unmarshal成JSON等各种序列化格式是个很常见的操作,特别是在构建微服务的时候。实际上,大部分微服务做的唯一工作就是序列化。像json.Marshal和json.Unmarshal需要依赖运行时反射才能将结构体的字段序列化成字节,反之亦然。这个操作很慢,反射的性能完全无法与显式的代码相比。

但我们不必这么做。marshalling JSON的原理大致如下:
package json

// Marshal take an object and returns its representation in JSON.
func Marshal(obj interface{}) ([]byte, error) {
// Check if this object knows how to marshal itself to JSON
// by satisfying the Marshaller interface.
if m, is := obj.(json.Marshaller); is {
return m.MarshalJSON()
}

// It doesn't know how to marshal itself. Do default reflection based marshallling.
return marshal(obj)
}如果我们知道怎样将对象marshal成JSON,就应该避免运行时反射。但我们不想手工marshal所有代码,怎么办呢?可以让计算机替我们写程序!像easyjson等代码生成器会检查结构体,然后生成高度优化且与json.Marshaller等接口完全兼容的代码。

下载这个包,然后在包含结构体的$file.go上运行下面的命令:
easyjson -all $file.go$file.go
这个命令会生成$file_easyjson.go。由于easyjson为我们实现了json.Marshaller接口,因此序列化时不会调用默认的反射,而是会使用生成的函数。祝贺你!你已经将JSON marshalling的代码的速度提高了三倍。还有许多其他技巧可以进一步提升性能。
 
使用strings.Builder来构建字符串
 
Go语言的字符串是不可修改的,可以认为它们是只读的字节切片。这就是说,每次创建字符串都要分配新的内存,可能还会给垃圾回收器造成更多工作。

Go 1.10引入了strings.Builder作为高效率构建字符串的方式。它内部会将字符串写入到字节缓冲区。只有在builder上调用String()时才会真正生成字符串。它依赖一些unsafe的技巧将底层的字节作为字符串返回,而不实际进行内存非配。这篇文章(https://syslog.ravelin.com/byt ... ca7ff)介绍了更多其工作原理。pkg: github.com/sjwhitworth/perfblog/strbuild
BenchmarkStringBuildNaive-8 5000000 255 ns/op 216 B/op 8 allocs/op
BenchmarkStringBuildBuilder-8 20000000 54.9 ns/op 64 B/op 1 allocs/op可见,strings.Builder要快4.7倍,它的内存分配次只有前者的1/8,内存使用只有前者的1/4。

所以,在性能重要的时候应该使用strings.Builder。一般来说,除非是非常不重要的情况,否则我建议永远使用strings.Builder来构建字符串。
 
使用strconv代替fmt

fmt是Go中最著名的包之一。估计你从第一个Go程序——输出“hello, world”——的时候就开始用它了。但是,如果需要将整数和浮点数转换成字符串,它就不如更底层的strconv有效率了。strconv只需要在API中进行很小改动,就能带来不错的性能提升。

大多数情况下fmt会接受一个interface{}作为参数。这样做有两个弊端:

失去了类型安全。对于我来说这是个很大的问题。

会增加内存分配次数。将非指针类型作为interface{}传递通常会导致堆分配的问题。进一步的内容可以阅读这篇文章(https://www.darkcoding.net/sof ... face/)。
BenchmarkStrconv-8 30000000 39.5 ns/op 32 B/op 1 allocs/op
BenchmarkFmt-8 10000000 143 ns/op 72 B/op 3 allocs/op可以看到,strconv版本要快3.5倍,内存分配次数是1/3,内存分配量是1/2。

在make中指定分配的容量来避免重新分配

在讨论性能改善之前,我们先来迅速看一下切片。切片是Go语言中一个非常有用的概念。它提供了可改变大小的数组,还可以用不同的方式表示同一片底层内存区域,而不需要重新进行内存分配。slice的内部结构由三个元素组成:

data:切片中指向底层数据的指针
len:切片中的当前元素数目
cap:在不重新分配内存的前提下,切片能够增长到的元素数目

我经常看到类似于下面的代码,尽管在切片容量可以预先得知的情况下依然生成一个容量为零的切片:var userIDs []string
for _, bar := range rsp.Users {
userIDs = append(userIDs, bar.ID)
}这段代码中,切片的初始长度和容量都为零。在收到响应后,我们将用户添加到切片。这样做就会达到切片的容量上限,从而导致底层分配两倍容量的新数组,然后将旧切片中的数据拷贝过来。如果有8个用户,就会造成5次内存分配。

更有效的方式是这样:
userIDs := make([]string, 0, len(rsp.Users)
for _, bar := range rsp.Users {
userIDs = append(userIDs, bar.ID)
}使用make显式声明切片的容量。接下来可以向切片添加元素,而不会触发内存重新分配和拷贝。
 
这条建议也适用于map:适用make(map[string]string, len(foo))可以分配足够的底层内存以避免内存重新分配。
 
使用可以接受字节切片的方法
 
在使用包时,寻找那些接受字节切片作为参数的方法,这些方法通常给你更多控制内存分配的自由。

一个很好的例子就是time.Format和time.AppendFormat。time.Format返回字符串。内部会分配一个新的字节切片,然后在其上调用time.AppendFormat。而time.AppendFormat接受一个字节缓冲区,将格式化后的时间写入缓冲区,然后返回扩展后的字节切片。标准库中这种做法非常常见,如strconv.AppendFloat或bytes.NewBuffer。

为什么这样能提高性能?因为你可以传递从sync.Poolh获得的字节切片,而不需要每次都分配新的缓冲区。或者可以初始化一个足够大的缓冲区,来减少切片拷贝。
 
这些建议只是某些具体情况下的建议,而不是真理。一定要自己测量性能。

要知道何时该停止优化。提高系统性能会让工程师感觉非常满足:问题本身很有趣,也有立竿见影的效果。但是,提高性能带来的效果非常依赖于具体情况。如果服务的响应时间只有10毫秒,而网络访问需要90毫秒,那么将10毫秒优化到5毫秒就完全不值得,因为你依然需要95毫秒。就算你将响应时间优化到1毫秒,最后结果还是91毫秒。你应该去做其他更有价值的事情。
 
原文:
https://stephen.sh/posts/quick-go-performance-improvements
译文:
https://blog.csdn.net/csdnnews/article/details/93987866
  查看全部
这篇文章中列出了一些不需要太多精力就能显著提高性能的技巧,并不包含那些需要太多精力或需要大幅度修改程序结构的技巧。
 
开始优化之前
 
开始优化之前,首先应该花些时间找出一个合适的基准线,以便稍后比较。如果没有基准,那就等于摸着石头过河,根本不知道自己的优化有没有效果。首先要编写性能测试程序,然后生成能用于pprof的profile文件。最好可以编写Go的性能测试脚本(https://dave.cheney.net/2013/0 ... in-go),这样可以很容易地使用pprof,还可以评测内存分配情况。还可以使用benchcmp,这个工具可以帮助比较两次性能测试之间的性能差异。

如果代码很难做性能测试,那就从你能测量时间的部分开始。可以利用runtime/pprof手工测量代码。

现在开始吧!
 
使用sync.Pool重用之前分配过的对象
 
sync.Pool实现了一个空闲列表(free-list)。这样可以重新使用之前分配过的对象。这样做可以将对象分配的代价平摊到多次使用上,减少垃圾回收器的工作。API非常简单:只需实现一个函数,用来分配新的对象即可。它会返回指针类型。
 
var bufpool = sync.Pool{
New: func() interface{} {
buf := make([]byte, 512)
return &buf
}}
之后,可以用Get()从池中获取对象,用完之后用Put()将对象放回。

不过要注意一些陷阱。在Go 1.13之前,每次发生垃圾回收时该池都会被清空。对于需要分配大量对象的程序来说,这可能会造成性能的影响。在1.13版本中似乎GC后能保留更多对象了(https://go-review.googlesource.com/c/go/+/162919/)。

你可能需要在将对象放回池中之前将其结构的字段清空。

如果不这样做,就可能从池中获得一个“脏”的对象,它包含之前使用过的数据。这可能会造成严重的安全问题!
 
安全的做法就是明确清空内存:
// reset resets all fields of the AuthenticationResponse before pooling it.
func (a* AuthenticationResponse) reset() {
a.Token = ""
a.UserID = ""
}

rsp := authPool.Get().(*AuthenticationResponse)
defer func() {
rsp.reset()
authPool.Put(rsp)
}()

在大map中避免使用包含指针的结构作为map的键
 
关于Go中大型堆的性能问题已经有很多人讨论过了。在垃圾回收过程中,运行时会扫描包含指针的对象并遍历其指针。如果你有非常大的map[string]int,那么垃圾回收器就不得不在每次垃圾回收过程中检查map中的每个字符串,因为字符串包含指针。

这个例子中我们向一个map[string]int中写入了一千万个元素,然后测量垃圾回收的时间。map是在包的作用域中分配的,以保证它被分配到堆上。
import (
"fmt"
"runtime"
"strconv"
"time"
)

const (
numElements = 10000000
)

var foo = map[string]int{}

func timeGC() {
t := time.Now()
runtime.GC()
fmt.Printf("gc took: %s\n", time.Since(t))
}

func main() {
for i := 0; i < numElements; i++ {
foo[strconv.Itoa(i)] = i
}

for {
timeGC()
time.Sleep(1 * time.Second)
}
}
运行后可以得到以下结果:
gc took: 98.726321ms
gc took: 105.524633ms
gc took: 102.829451ms
gc took: 102.71908ms
gc took: 103.084104ms
gc took: 104.821989ms
对于计算机来说这花得时间太多了!

怎样可以改进呢?最好是能尽量去掉指针,这样能减少垃圾回收器需要遍历的指针数量。由于字符串包含指针,因此我们可以用map[int]int来实现:
 
import (
"fmt"
"runtime"
"time"
)

const (
numElements = 10000000
)

var foo = map[int]int{}

func timeGC() {
t := time.Now()
runtime.GC()
fmt.Printf("gc took: %s\n", time.Since(t))
}

func main() {
for i := 0; i < numElements; i++ {
foo[i] = i
}

for {
timeGC()
time.Sleep(1 * time.Second)
}
}
重新运行程序,结果如下:
 
gc took: 3.608993ms
gc took: 3.926913ms
gc took: 3.955706ms
gc took: 4.063795ms
gc took: 3.91519ms
gc took: 3.75226ms
好多了。垃圾回收的时间减少了97%。在生产环境下,字符串需要进行hash之后再插入到map中。
 
生成marshalling代码以避免运行时反射
 
将数据结构marshalh或unmarshal成JSON等各种序列化格式是个很常见的操作,特别是在构建微服务的时候。实际上,大部分微服务做的唯一工作就是序列化。像json.Marshal和json.Unmarshal需要依赖运行时反射才能将结构体的字段序列化成字节,反之亦然。这个操作很慢,反射的性能完全无法与显式的代码相比。

但我们不必这么做。marshalling JSON的原理大致如下:
package json

// Marshal take an object and returns its representation in JSON.
func Marshal(obj interface{}) ([]byte, error) {
// Check if this object knows how to marshal itself to JSON
// by satisfying the Marshaller interface.
if m, is := obj.(json.Marshaller); is {
return m.MarshalJSON()
}

// It doesn't know how to marshal itself. Do default reflection based marshallling.
return marshal(obj)
}
如果我们知道怎样将对象marshal成JSON,就应该避免运行时反射。但我们不想手工marshal所有代码,怎么办呢?可以让计算机替我们写程序!像easyjson等代码生成器会检查结构体,然后生成高度优化且与json.Marshaller等接口完全兼容的代码。

下载这个包,然后在包含结构体的$file.go上运行下面的命令:
easyjson -all $file.go$file.go

这个命令会生成$file_easyjson.go。由于easyjson为我们实现了json.Marshaller接口,因此序列化时不会调用默认的反射,而是会使用生成的函数。祝贺你!你已经将JSON marshalling的代码的速度提高了三倍。还有许多其他技巧可以进一步提升性能。
 
使用strings.Builder来构建字符串
 
Go语言的字符串是不可修改的,可以认为它们是只读的字节切片。这就是说,每次创建字符串都要分配新的内存,可能还会给垃圾回收器造成更多工作。

Go 1.10引入了strings.Builder作为高效率构建字符串的方式。它内部会将字符串写入到字节缓冲区。只有在builder上调用String()时才会真正生成字符串。它依赖一些unsafe的技巧将底层的字节作为字符串返回,而不实际进行内存非配。这篇文章(https://syslog.ravelin.com/byt ... ca7ff)介绍了更多其工作原理。
pkg: github.com/sjwhitworth/perfblog/strbuild
BenchmarkStringBuildNaive-8 5000000 255 ns/op 216 B/op 8 allocs/op
BenchmarkStringBuildBuilder-8 20000000 54.9 ns/op 64 B/op 1 allocs/op
可见,strings.Builder要快4.7倍,它的内存分配次只有前者的1/8,内存使用只有前者的1/4。

所以,在性能重要的时候应该使用strings.Builder。一般来说,除非是非常不重要的情况,否则我建议永远使用strings.Builder来构建字符串。
 
使用strconv代替fmt

fmt是Go中最著名的包之一。估计你从第一个Go程序——输出“hello, world”——的时候就开始用它了。但是,如果需要将整数和浮点数转换成字符串,它就不如更底层的strconv有效率了。strconv只需要在API中进行很小改动,就能带来不错的性能提升。

大多数情况下fmt会接受一个interface{}作为参数。这样做有两个弊端:

失去了类型安全。对于我来说这是个很大的问题。

会增加内存分配次数。将非指针类型作为interface{}传递通常会导致堆分配的问题。进一步的内容可以阅读这篇文章(https://www.darkcoding.net/sof ... face/)。
BenchmarkStrconv-8      30000000            39.5 ns/op        32 B/op          1 allocs/op
BenchmarkFmt-8 10000000 143 ns/op 72 B/op 3 allocs/op
可以看到,strconv版本要快3.5倍,内存分配次数是1/3,内存分配量是1/2。

在make中指定分配的容量来避免重新分配

在讨论性能改善之前,我们先来迅速看一下切片。切片是Go语言中一个非常有用的概念。它提供了可改变大小的数组,还可以用不同的方式表示同一片底层内存区域,而不需要重新进行内存分配。slice的内部结构由三个元素组成:


data:切片中指向底层数据的指针
len:切片中的当前元素数目
cap:在不重新分配内存的前提下,切片能够增长到的元素数目


我经常看到类似于下面的代码,尽管在切片容量可以预先得知的情况下依然生成一个容量为零的切片:
var userIDs []string
for _, bar := range rsp.Users {
userIDs = append(userIDs, bar.ID)
}
这段代码中,切片的初始长度和容量都为零。在收到响应后,我们将用户添加到切片。这样做就会达到切片的容量上限,从而导致底层分配两倍容量的新数组,然后将旧切片中的数据拷贝过来。如果有8个用户,就会造成5次内存分配。

更有效的方式是这样:
userIDs := make([]string, 0, len(rsp.Users)
for _, bar := range rsp.Users {
userIDs = append(userIDs, bar.ID)
}
使用make显式声明切片的容量。接下来可以向切片添加元素,而不会触发内存重新分配和拷贝。
 
这条建议也适用于map:适用make(map[string]string, len(foo))可以分配足够的底层内存以避免内存重新分配。
 
使用可以接受字节切片的方法
 
在使用包时,寻找那些接受字节切片作为参数的方法,这些方法通常给你更多控制内存分配的自由。

一个很好的例子就是time.Format和time.AppendFormat。time.Format返回字符串。内部会分配一个新的字节切片,然后在其上调用time.AppendFormat。而time.AppendFormat接受一个字节缓冲区,将格式化后的时间写入缓冲区,然后返回扩展后的字节切片。标准库中这种做法非常常见,如strconv.AppendFloat或bytes.NewBuffer。

为什么这样能提高性能?因为你可以传递从sync.Poolh获得的字节切片,而不需要每次都分配新的缓冲区。或者可以初始化一个足够大的缓冲区,来减少切片拷贝。
 
这些建议只是某些具体情况下的建议,而不是真理。一定要自己测量性能。

要知道何时该停止优化。提高系统性能会让工程师感觉非常满足:问题本身很有趣,也有立竿见影的效果。但是,提高性能带来的效果非常依赖于具体情况。如果服务的响应时间只有10毫秒,而网络访问需要90毫秒,那么将10毫秒优化到5毫秒就完全不值得,因为你依然需要95毫秒。就算你将响应时间优化到1毫秒,最后结果还是91毫秒。你应该去做其他更有价值的事情。
 
原文:
https://stephen.sh/posts/quick-go-performance-improvements
译文:
https://blog.csdn.net/csdnnews/article/details/93987866
 

#面试题集锦#实现单链表反转

面试zkbhj 发表了文章 • 0 个评论 • 134 次浏览 • 2020-08-14 17:16 • 来自相关话题

反转一个链表

示例:输入: 1->2->3->4->5->NULL
输出: 5->4->3->2->1->NULL
结构体定义struct ListNode {
int val;
struct ListNode *next;
};
 





 思路一

先对原链表做头删操作,再对新链表做头插
定义一个新head头指针,标记为newHead,将它初始为NULL,并非指向NULL,最后我们选择返回这个newHead指针作为新链表的头指针。
定义一个结点node作为"临时中转站",初始化与否并无大影响。进行循环遍历链表各个结点,判定head指针是否为空,即是否到达了原链表的结尾。如果不为空,说明还没有到达尾部。如果程序第一次运行就没有进入循环,说明传入了一个空链表,此时返回newHead新链表的头指针,同样返回NULL,这样处理也是合理的。以下开始逆序链表逻辑:在当前指针不为NULL时,先对原链表做头删操作,再对新链表做头插操作。即使用循环进行操作:让node指针指向传入函数链表的头指针head,两指针指向保持相同。然后让head指针指向它的next结点,此时旧链表已经完成了头删操作。第一个结点已经被"切割"下来。接下来要做的就是对新链表进行头插操作,使结点放入新链表。让node指针的next下一个结点指向新链表的头指针newHead,完成结点的链接,即头插入新的链表中。然后更新newHead指向为新链表的头结点。进行下一次循环。最终head指针指向了原链表的结尾,即为NULL,退出循环,此时新链表已经反转完毕,情况如图:最终返回新链表头指针newHead即可。





 struct ListNode *reverseList(struct ListNode* head) {
struct ListNode *newHead = NULL;
struct ListNode *node;
while (head != NULL) {
//1. 对之前的链表做头删
node = head;
head = head->next;

//2. 对新链表做头插
node->next = newHead;
newHead = node;
}
return newHead;
}
思路二





利用选择语句,找到空链表的情况,此情况返回NULL空指针,因为空链表不能反转,或者说反转之后还是一个空链表,返回空。利用三个指针p0"前指针"、p1“当前指针”、p2"后指针"来分批处理链表元素,p0置为NULL,它将作为链表的尾结点向前推进处理,p1指向旧链表的头指针head,p2指向旧链表的头指针的next结点。开始遍历链表,循环判定因子为p1,当它为空时到达链表尾部跳出循环。否则在表中执行循环内逻辑:将p1指向的当前结点的下一个结点指向p0,即前一个结点。此时p0为NULL,那么p1的下一个结点就为空了,它现在是最后一个结点。然后将p0指针指向p1,将p1指针指向p2,注意这两步不可以调换顺序,否则正确向后挪移一位。此时完成了三个指针的一轮更迭。判定p2指针是否为空,如果为空说明此时p2到达了链表结尾,当前指针p1的指向为最后一个结点,它的next即为空。如果不为空,将p2更新到下一个结点,进行下一次循环。下一次进行循环时,就会把截断结点链接到新链表的头部,同时更新三个指针。继续循环。循环终止条件为:p1指向了链表尾部的NULL,此时p1的前指针p0即指向了反转后的链表,它就是新链表的head头指针。此时返回p0即可。





 struct ListNode *reverseList(struct ListNode* head) {
if (head == NULL) {
return NULL;
}
struct ListNode *p0 = NULL;
struct ListNode *p1 = head;
struct ListNode *p2 = head->next;
while (p1 != NULL) {
p1->next = p0;

p0 = p1;
p1 = p2;
if (p2 != NULL) {
p2 = p2->next;
}
}
return p0;
}
参考文档:
https://blog.csdn.net/qq_42351880/article/details/88637387
 
  查看全部
反转一个链表

示例:
输入: 1->2->3->4->5->NULL
输出: 5->4->3->2->1->NULL

结构体定义
struct ListNode {
int val;
struct ListNode *next;
};

 

QQ截图20200814170600.jpg

 思路一

先对原链表做头删操作,再对新链表做头插
  1. 定义一个新head头指针,标记为newHead,将它初始为NULL,并非指向NULL,最后我们选择返回这个newHead指针作为新链表的头指针。

  1. 定义一个结点node作为"临时中转站",初始化与否并无大影响。
  2. 进行循环遍历链表各个结点,判定head指针是否为空,即是否到达了原链表的结尾。如果不为空,说明还没有到达尾部。如果程序第一次运行就没有进入循环,说明传入了一个空链表,此时返回newHead新链表的头指针,同样返回NULL,这样处理也是合理的。
  3. 以下开始逆序链表逻辑:在当前指针不为NULL时,先对原链表做头删操作,再对新链表做头插操作。即使用循环进行操作:
  4. 让node指针指向传入函数链表的头指针head,两指针指向保持相同。
  5. 然后让head指针指向它的next结点,此时旧链表已经完成了头删操作。第一个结点已经被"切割"下来。接下来要做的就是对新链表进行头插操作,使结点放入新链表。
  6. 让node指针的next下一个结点指向新链表的头指针newHead,完成结点的链接,即头插入新的链表中。然后更新newHead指向为新链表的头结点。进行下一次循环。
  7. 最终head指针指向了原链表的结尾,即为NULL,退出循环,此时新链表已经反转完毕,情况如图:
  8. 最终返回新链表头指针newHead即可。


QQ截图20200814170610.jpg

 
struct ListNode *reverseList(struct ListNode* head) {
struct ListNode *newHead = NULL;
struct ListNode *node;
while (head != NULL) {
//1. 对之前的链表做头删
node = head;
head = head->next;

//2. 对新链表做头插
node->next = newHead;
newHead = node;
}
return newHead;
}

思路二

QQ截图20200814171211.jpg

  1. 利用选择语句,找到空链表的情况,此情况返回NULL空指针,因为空链表不能反转,或者说反转之后还是一个空链表,返回空。
  2. 利用三个指针p0"前指针"、p1“当前指针”、p2"后指针"来分批处理链表元素,p0置为NULL,它将作为链表的尾结点向前推进处理,p1指向旧链表的头指针head,p2指向旧链表的头指针的next结点。
  3. 开始遍历链表,循环判定因子为p1,当它为空时到达链表尾部跳出循环。否则在表中执行循环内逻辑:将p1指向的当前结点的下一个结点指向p0,即前一个结点。此时p0为NULL,那么p1的下一个结点就为空了,它现在是最后一个结点。
  4. 然后将p0指针指向p1,将p1指针指向p2,注意这两步不可以调换顺序,否则正确向后挪移一位。此时完成了三个指针的一轮更迭。
  5. 判定p2指针是否为空,如果为空说明此时p2到达了链表结尾,当前指针p1的指向为最后一个结点,它的next即为空。如果不为空,将p2更新到下一个结点,进行下一次循环。
  6. 下一次进行循环时,就会把截断结点链接到新链表的头部,同时更新三个指针。继续循环。
  7. 循环终止条件为:p1指向了链表尾部的NULL,此时p1的前指针p0即指向了反转后的链表,它就是新链表的head头指针。此时返回p0即可。


QQ截图20200814171221.jpg

 
struct ListNode *reverseList(struct ListNode* head) {
if (head == NULL) {
return NULL;
}
struct ListNode *p0 = NULL;
struct ListNode *p1 = head;
struct ListNode *p2 = head->next;
while (p1 != NULL) {
p1->next = p0;

p0 = p1;
p1 = p2;
if (p2 != NULL) {
p2 = p2->next;
}
}
return p0;
}

参考文档:
https://blog.csdn.net/qq_42351880/article/details/88637387
 
 

#每日精进#2020年8月6日

总结zkbhj 发表了文章 • 0 个评论 • 144 次浏览 • 2020-08-06 09:31 • 来自相关话题

【早读:《深入理解计算机系统》】

第二章 信息的表示和处理

表示代码int sum(int x, int y) {
return x + y;
}当我们在不同的机器上编译上面的程序得到的机器代码都不尽相同:

Linux32     55 89 e5 8b 45 0c 03 45 08 c9 c3
Windows  55 89 e5 8b 45 0c 03 45 08 5d c3
Sun           81 c3 e0 08 90 03 00 09


因此,二进制代码是不兼容的,无法在不同机器之间移植。

这得到一个计算机系统的基本概念:从机器的角度来看,程序仅仅只是字节序列,机器没有关于原始程序的任何信息。

布尔代数简介

计算机的核心都是围绕1和0来演化的。对于0和1的起源,要追溯到1850年前后乔治·布尔的工作,所以这个也叫布尔代数。即通过将逻辑值TRUE和FALSE编码为二进制1和0设计出的一种代数,以研究逻辑推理的基本原则。






上面分别列出了~(NOT)、&(AND)、|(OR)和^(EXCLUSIVE-OR)四种基本运算。

后来创立信息论领域的Claude Shannon首先建立了布尔代数和数字逻辑之间的联系。

将上述基础的布尔运算扩展到位向量运算。位向量就是固定长度为w、由0和1组成的串。

假设 w=4,a=[0110],b=[1100]。那么四种运算 a&b、a|b、a^b、~b 结果分别如下:





 
布尔运算&对|满足分配率:a&(b|c) = (a&b)|(a&c);反过来,|也满足对&的分配率,即:a|(b&c) = (a|b) & (a|c)。

位向量的一个有用应用就是表示有限集合,即用位向量来给集合进行编码。

C语言的一个有用特性就是支持按位布尔运算。|、&、~、^这些运算可以用到任何“整型”的数据类型上。void inplace_swap(int *x, int *y){
*y = *x ^ *y;
*x = *x ^ *y;
*y = *x ^ *y;
}
上面这段代码,就是利用了两个事实来实现*x和*y所指向的变量值进行了交换操作。两个事实分别是:

异或运算是可交换和可结合的;

对于任意的a, a ^ a = 0;

所以上述程序的计算过程如下:

初始:*x = a    *y = b
第一步:*x = a   *y = a ^ b
第二步:*x = a ^ (a ^ b) = (a ^ a) ^ b = b   *y = a ^ b
第三部:*x = b   *y = b ^ ( a ^ b) = (b ^ b) ^ a = a


但是注意,这种方式和通常的交换两个数值的技术不一样,当移动一个值时,我们不需要第三个位置来临时存放另外一个值。这种交换方式并没有性能上的优势,它仅仅是一个智力游戏!

位级运算常见的用法就是实现掩码运算:掩码是一个位模式,表示从一个字中选出的位的集合。

比如对于掩码0xFF(最低的8位都是1)表示一个字的低位字节。x&0xFF会得到一个由x的最低有效字节组成的值。 
 
 
【英文中几点钟的说法o'clock是什么的缩写?】

o'clock = of the clock.

在14世纪以前,人类还没有发明出来时钟,都是通过一些其他途径来获取和感知时间,比如日晷、沙漏等。直到 14 世纪,现代意义上的时钟雏形才得以发明。当时的时钟会自己报时“说出”:It's 7 of the clock!后来,随着时钟的普及和大众化,人们开始将 of 的 f 和 the 一带而过,简略地读成了 o'clock。
 https://ask.zkbhj.com/?/article/370 查看全部

【早读:《深入理解计算机系统》】

第二章 信息的表示和处理

表示代码
int sum(int x, int y) {
return x + y;
}
当我们在不同的机器上编译上面的程序得到的机器代码都不尽相同:


Linux32     55 89 e5 8b 45 0c 03 45 08 c9 c3
Windows  55 89 e5 8b 45 0c 03 45 08 5d c3
Sun           81 c3 e0 08 90 03 00 09



因此,二进制代码是不兼容的,无法在不同机器之间移植。

这得到一个计算机系统的基本概念:从机器的角度来看,程序仅仅只是字节序列,机器没有关于原始程序的任何信息。

布尔代数简介

计算机的核心都是围绕1和0来演化的。对于0和1的起源,要追溯到1850年前后乔治·布尔的工作,所以这个也叫布尔代数。即通过将逻辑值TRUE和FALSE编码为二进制1和0设计出的一种代数,以研究逻辑推理的基本原则。

20200806092721.jpg


上面分别列出了~(NOT)、&(AND)、|(OR)和^(EXCLUSIVE-OR)四种基本运算。

后来创立信息论领域的Claude Shannon首先建立了布尔代数和数字逻辑之间的联系。

将上述基础的布尔运算扩展到位向量运算。位向量就是固定长度为w、由0和1组成的串。

假设 w=4,a=[0110],b=[1100]。那么四种运算 a&b、a|b、a^b、~b 结果分别如下:

20200806092735.jpg

 
布尔运算&对|满足分配率:a&(b|c) = (a&b)|(a&c);反过来,|也满足对&的分配率,即:a|(b&c) = (a|b) & (a|c)。

位向量的一个有用应用就是表示有限集合,即用位向量来给集合进行编码。

C语言的一个有用特性就是支持按位布尔运算。|、&、~、^这些运算可以用到任何“整型”的数据类型上。
void inplace_swap(int *x, int *y){
*y = *x ^ *y;
*x = *x ^ *y;
*y = *x ^ *y;
}

上面这段代码,就是利用了两个事实来实现*x和*y所指向的变量值进行了交换操作。两个事实分别是:

异或运算是可交换和可结合的;

对于任意的a, a ^ a = 0;

所以上述程序的计算过程如下:


初始:*x = a    *y = b
第一步:*x = a   *y = a ^ b
第二步:*x = a ^ (a ^ b) = (a ^ a) ^ b = b   *y = a ^ b
第三部:*x = b   *y = b ^ ( a ^ b) = (b ^ b) ^ a = a



但是注意,这种方式和通常的交换两个数值的技术不一样,当移动一个值时,我们不需要第三个位置来临时存放另外一个值。这种交换方式并没有性能上的优势,它仅仅是一个智力游戏!

位级运算常见的用法就是实现掩码运算:掩码是一个位模式,表示从一个字中选出的位的集合。

比如对于掩码0xFF(最低的8位都是1)表示一个字的低位字节。x&0xFF会得到一个由x的最低有效字节组成的值。 
 
 
英文中几点钟的说法o'clock是什么的缩写?】


o'clock = of the clock.


在14世纪以前,人类还没有发明出来时钟,都是通过一些其他途径来获取和感知时间,比如日晷、沙漏等。直到 14 世纪,现代意义上的时钟雏形才得以发明。当时的时钟会自己报时“说出”:It's 7 of the clock!后来,随着时钟的普及和大众化,人们开始将 of 的 f 和 the 一带而过,简略地读成了 o'clock。
 https://ask.zkbhj.com/?/article/370

#每日精进#2020年8月5日

总结zkbhj 发表了文章 • 0 个评论 • 155 次浏览 • 2020-08-05 11:20 • 来自相关话题

【早读:《深入理解计算机系统》】

第二章 信息的表示和处理

寻址和字节顺序

在几乎所有机器上,多字节对象被存储为连续的字节序列,对象的地址为最小的地址。

排列表示一个对象的字节有两个通用的规则:

小端法:在内存中按照从最低有效字节到最高有效字节的顺序存储对象;
大端法:在内存中按照从最高有效字节到最低有效字节的顺序存储对象;一旦选择了特定的操作系统,那么字节顺序也就固定下来。Android和IOS只能够运行于小端模式下。

两种方式没有谁好谁坏之分,对于那种字节排序的选择都是任意的。
大小端的说法来自于Jonathan Swift的《格利弗游记》一书,其中交战的两个派别无法就打开一个半熟的鸡蛋应该从哪一端打开达成一致意见。
不同的端模式下,会有以下影响:
1、通过网络在不同端模式的机器间传递数据时,发送和接收时,需要转换网络标准;
2、在检查机器级程序时,对数据的解读方式;
3、在编写规避正常的类型系统的程序时。​#include <stdio.h>

typedef unsigned char *byte_pointer;
//typedef char *byte_pointer;

void show_bytes(byte_pointer start, size_t len) {
size_t i;
for (i = 0; i < len; i++)
printf("%p\t0x%.2x\n", &start, start);
printf("\n");
}

void show_int(int x) {
show_bytes((byte_pointer) &x, sizeof(int));
}

void show_float(float x) {
show_bytes((byte_pointer) &x, sizeof(float));
}

void show_pointer(void *x) {
show_bytes((byte_pointer) &x, sizeof(void *));
}

​在这段程序中,“byte_pointer start”告诉编译器,应该把这个指针看成指向一个字节序列,而不是指向一个原始数据类型的对象。然后,这个指针会被看成是对象使用的最低字节地址。
这种强制类型转换不会改变真实的指针,它们只是告诉编译器以新的数据类型来看待被指向的数据。
使用ASCII码作为字符码的任何系统上都是将得到相同的结果,与字节顺序和字节大小规则无关。因而,文本数据比二进制数据具有更强的平台独立性。
 
【HTML页面里怎么实现代码包含?】
https://ask.zkbhj.com/?/question/387
 
【Yii2框架中如何区分不同的场景指定赋值字段和检验规则】
 
场景(scenario)

分析上面问题,会发现关键点是批量赋值(massive assignment)和数据校验(validate)两个方法。如果对不同的场景指定赋值字段和检验规则,问题就迎刃而解。

Yii中的scenario有 安全属性 和 活跃属性 两个概念。安全属性用在批量赋值的load方法,只有安全属性才能被赋值;活跃属性用在规则校验的validate方法,在活跃属性集中并且定义了校验规则的属性才会被校验。活跃属性和安全属性的关系是,安全属性是活跃属性的子集。

\yii\base\Model类定义了默认场景:SCENARIO_DEFAULT(值为default)。默认场景下,出现在rules方法中的属性既是活跃属性,又是安全属性(这句话基本正确,看后续解释)。为不同场景指定活跃属性、安全属性以及校验器,可以通过覆盖senarios或rules两个方法实现(几乎每个Model类都会重写rules方法,senarios用得少)。

rules

先看rules方法。默认的属性加校验器定义方式,让每个属性既是安全属性,也是活跃属性。如果想让某个属性不是安全属性(不能通过load批量赋值),在属性名前加感叹号!即可。例如student中的user_id字段:public function rules()
{
return [
["!user_od", "required"],
["!user_id", "integer"],
["!user_od", "unique"],
// other rules
];
}user_id是活跃属性,在写入数据库时会被校验。但它不是安全属性,不能通过load方法进行赋值,解决了安全隐患。

再看rules方法按场景区分校验器规则的做法:定义校验器时on属性指定规则在哪些场景下生效,except属性则排除一些场景(如果不指定on和except,规则对所有场景生效)。例如:public function rules()
{
return [
["password", "string", "length" => [8, 16], "on" => ["signup"]], // 仅在signup场景时才被验证
["status", "integer", "except" => ["signup"], // 除了signup场景,其他情况都校验
// other rules
];
}在原来基础上新增感叹号和on/except属性,非常简便的就定义了非安全属性以及分场景指定校验规则。

scenarios

另外一种更清晰定义安全属性和活跃属性的做法是重写scenarios方法。scenarios方法返回一个数组,数组的键是场景名称,值是活跃属性集合(包饭安全属性)。例如student表的可能实现如下:public function scenarios()
{
return [
self::SCENARIO_DEFAULT => ["!user_id", "grade", "class", xxxx],
"update" => ["grade", "class", xxxx],
];
}默认情形下(学生报名),年级、班级这些信息是安全属性,但user_id不是,只能在程序内部赋值,并在插入数据时被校验;在修改信息时,user_id不是活跃属性,既不能被批量赋值,也不需要校验(事实上它不应该改变)。

scenarios方法只能定义活跃属性和安全属性,无法定义校验规则,需要和rules配合使用。
https://www.cnblogs.com/yangxunwu1992/p/6669380.html

 
【关于未来几年国内经济形势的分析总结】】
 
疫情发生以来,国内外经济形势发生了重大变化。国际局势也从全球化走向了逆全球化的道路,美国通过各种手段(贸易战、“中国病毒”论、打压华为、打压Tik Tok、关闭领事馆、干涉中国内政等)不断打压中国,各大企业也在不断将企业撤出中国。
 
最近最热门的经济词汇就是经济内循环,意思是说,以后要以国内市场为主,国内生产出来的东西,主要在自己国内消化掉,重新转化为生产力。也就是走内需拉动经济增长的路子。这是在疫情发生以及国内外形式发生如此变化之后的无奈之举。
 
我们过去拉动经济的三驾马车是外贸,基建和房地产,内需一向很薄弱,这几年内需份额有所提高,但还不足以成为拉动经济增长的主要动力。
 
最主要体现在居民收入不足,杠杆过高,中产被绑在房地产的战车上面等。
 
从微观层面看,中国消费结构呈现两边高中间低的M字型结构(健康的结构应该是相反的,两边低中间高,中产成为消费的主力)。所以中国目前的情况就是有钱人并不会受到高房价的影响而降低消费,反而还因此提高了消费能力,还在消费升级。然后相对经济收入低的人群消费能力本就低,地摊经济很火就说明这个问题。中产,由于购房等压力,从中端消费跌落到低端消费。
 
所以,国内要实现以经济内循环为主,国际循环为辅的国内国际双循环的新发展格局,有以下几种办法:
1、提高居民购买力
主要就是降房价和提高居民收入,提高居民收入很难,现在经济太差,没有需求就没有工作岗位,居民收入不下降已经很不错,但房价是有可能缓慢下降的

2、发展房地产。
过去十几年就是这么干的,但现在房价太高,已经到了伤害经济的底部,已经不能继续走拉抬房价发展经济的老路。

3、让股市走出慢牛。
目前看也相对可行,上层也一直在强调要通过资本市场服务实体经济,这也是我一向看好A股走牛的重要原因。
 
4、发展高科技和人民币国际化。
 
讲通俗一点,就是:

经济内循环其实就是要过苦日子的另一个代名词。也可以简单理解为好好干活,但不要想着能挣很多钱。在未来很长一段时间里面,物价会相对比较便宜。

 
https://zhuanlan.zhihu.com/p/165347415
 
【Go语言核心36讲:第12节 使用函数的正确姿势(1)】
 
在 Go 语言中,函数可是一等的(first-class)公民,函数类型也是一等的数据类型(函数类型属于引用类型,它的零值为nil)。
这意味着函数不但可以用于封装代码、分割功能、解耦逻辑,还可以化身为普通的值,在其他函数间传递、赋予变量、做类型判断和转换等。“函数是一等的公民”是函数式编程(functional programming)的重要特征。Go 语言在语言层面支持了函数式编程。

package main

import "fmt"

type Printer func(contents string) (n int, err error)

func printToStd(contents string) (bytesNum int, err error) {
return fmt.Println(contents)
}

func main() {
var p Printer
p = printToStd
p("something")
}函数的签名其实就是函数的参数列表和结果列表的统称,它定义了可用来鉴别不同函数的那些特征,同时也定义了我们与函数交互的方式。

各个参数和结果的名称不能算作函数签名的一部分,对于结果声明,名字也可以没有。且函数的名称也不算函数签名的一部分,只是个标识符而已。

高阶函数
 

1. 接受其他的函数作为参数传入;
2. 把其他的函数作为结果返回。

只要满足了其中任意一个特点,我们就可以说这个函数是一个高阶函数。
 
卫述语句
 
卫述语句是指被用来检查关键的先决条件的合法性,并在检查未通过的情况下立即终止当前代码块执行的语句。在 Go 语言中,if 语句常被作为卫述语句。
if op == nil {
return 0, errors.New("invalid operation")
} 查看全部
【早读:《深入理解计算机系统》】

第二章 信息的表示和处理

寻址和字节顺序

在几乎所有机器上,多字节对象被存储为连续的字节序列,对象的地址为最小的地址。

排列表示一个对象的字节有两个通用的规则:


小端法:在内存中按照从最低有效字节到最高有效字节的顺序存储对象;
大端法:在内存中按照从最高有效字节到最低有效字节的顺序存储对象;一旦选择了特定的操作系统,那么字节顺序也就固定下来。Android和IOS只能够运行于小端模式下。


两种方式没有谁好谁坏之分,对于那种字节排序的选择都是任意的。
大小端的说法来自于Jonathan Swift的《格利弗游记》一书,其中交战的两个派别无法就打开一个半熟的鸡蛋应该从哪一端打开达成一致意见。
不同的端模式下,会有以下影响:
1、通过网络在不同端模式的机器间传递数据时,发送和接收时,需要转换网络标准;
2、在检查机器级程序时,对数据的解读方式;
3、在编写规避正常的类型系统的程序时。
​#include <stdio.h>

typedef unsigned char *byte_pointer;
//typedef char *byte_pointer;

void show_bytes(byte_pointer start, size_t len) {
size_t i;
for (i = 0; i < len; i++)
printf("%p\t0x%.2x\n", &start, start);
printf("\n");
}

void show_int(int x) {
show_bytes((byte_pointer) &x, sizeof(int));
}

void show_float(float x) {
show_bytes((byte_pointer) &x, sizeof(float));
}

void show_pointer(void *x) {
show_bytes((byte_pointer) &x, sizeof(void *));
}

​在这段程序中,“byte_pointer start”告诉编译器,应该把这个指针看成指向一个字节序列,而不是指向一个原始数据类型的对象。然后,这个指针会被看成是对象使用的最低字节地址。
这种强制类型转换不会改变真实的指针,它们只是告诉编译器以新的数据类型来看待被指向的数据。
使用ASCII码作为字符码的任何系统上都是将得到相同的结果,与字节顺序和字节大小规则无关。因而,文本数据比二进制数据具有更强的平台独立性。
 
【HTML页面里怎么实现代码包含?】
https://ask.zkbhj.com/?/question/387
 
【Yii2框架中如何区分不同的场景指定赋值字段和检验规则】
 
场景(scenario)

分析上面问题,会发现关键点是批量赋值(massive assignment)和数据校验(validate)两个方法。如果对不同的场景指定赋值字段和检验规则,问题就迎刃而解。

Yii中的scenario有 安全属性 和 活跃属性 两个概念。安全属性用在批量赋值的load方法,只有安全属性才能被赋值;活跃属性用在规则校验的validate方法,在活跃属性集中并且定义了校验规则的属性才会被校验。活跃属性和安全属性的关系是,安全属性是活跃属性的子集。

\yii\base\Model类定义了默认场景:SCENARIO_DEFAULT(值为default)。默认场景下,出现在rules方法中的属性既是活跃属性,又是安全属性(这句话基本正确,看后续解释)。为不同场景指定活跃属性、安全属性以及校验器,可以通过覆盖senarios或rules两个方法实现(几乎每个Model类都会重写rules方法,senarios用得少)。

rules

先看rules方法。默认的属性加校验器定义方式,让每个属性既是安全属性,也是活跃属性。如果想让某个属性不是安全属性(不能通过load批量赋值),在属性名前加感叹号!即可。例如student中的user_id字段:
public function rules()
{
return [
["!user_od", "required"],
["!user_id", "integer"],
["!user_od", "unique"],
// other rules
];
}
user_id是活跃属性,在写入数据库时会被校验。但它不是安全属性,不能通过load方法进行赋值,解决了安全隐患。

再看rules方法按场景区分校验器规则的做法:定义校验器时on属性指定规则在哪些场景下生效,except属性则排除一些场景(如果不指定on和except,规则对所有场景生效)。例如:
public function rules()
{
return [
["password", "string", "length" => [8, 16], "on" => ["signup"]], // 仅在signup场景时才被验证
["status", "integer", "except" => ["signup"], // 除了signup场景,其他情况都校验
// other rules
];
}
在原来基础上新增感叹号和on/except属性,非常简便的就定义了非安全属性以及分场景指定校验规则。

scenarios

另外一种更清晰定义安全属性和活跃属性的做法是重写scenarios方法。scenarios方法返回一个数组,数组的键是场景名称,值是活跃属性集合(包饭安全属性)。例如student表的可能实现如下:
public function scenarios()
{
return [
self::SCENARIO_DEFAULT => ["!user_id", "grade", "class", xxxx],
"update" => ["grade", "class", xxxx],
];
}
默认情形下(学生报名),年级、班级这些信息是安全属性,但user_id不是,只能在程序内部赋值,并在插入数据时被校验;在修改信息时,user_id不是活跃属性,既不能被批量赋值,也不需要校验(事实上它不应该改变)。

scenarios方法只能定义活跃属性和安全属性,无法定义校验规则,需要和rules配合使用。
https://www.cnblogs.com/yangxunwu1992/p/6669380.html

 
【关于未来几年国内经济形势的分析总结】】
 
疫情发生以来,国内外经济形势发生了重大变化。国际局势也从全球化走向了逆全球化的道路,美国通过各种手段(贸易战、“中国病毒”论、打压华为、打压Tik Tok、关闭领事馆、干涉中国内政等)不断打压中国,各大企业也在不断将企业撤出中国。
 
最近最热门的经济词汇就是经济内循环,意思是说,以后要以国内市场为主,国内生产出来的东西,主要在自己国内消化掉,重新转化为生产力。也就是走内需拉动经济增长的路子。这是在疫情发生以及国内外形式发生如此变化之后的无奈之举。
 
我们过去拉动经济的三驾马车是外贸,基建和房地产,内需一向很薄弱,这几年内需份额有所提高,但还不足以成为拉动经济增长的主要动力。
 
最主要体现在居民收入不足杠杆过高中产被绑在房地产的战车上面等。
 
从微观层面看,中国消费结构呈现两边高中间低的M字型结构(健康的结构应该是相反的,两边低中间高,中产成为消费的主力)。所以中国目前的情况就是有钱人并不会受到高房价的影响而降低消费,反而还因此提高了消费能力,还在消费升级。然后相对经济收入低的人群消费能力本就低,地摊经济很火就说明这个问题。中产,由于购房等压力,从中端消费跌落到低端消费。
 
所以,国内要实现以经济内循环为主,国际循环为辅的国内国际双循环的新发展格局,有以下几种办法:
1、提高居民购买力
主要就是降房价和提高居民收入,提高居民收入很难,现在经济太差,没有需求就没有工作岗位,居民收入不下降已经很不错,但房价是有可能缓慢下降的

2、发展房地产。
过去十几年就是这么干的,但现在房价太高,已经到了伤害经济的底部,已经不能继续走拉抬房价发展经济的老路。

3、让股市走出慢牛。
目前看也相对可行,上层也一直在强调要通过资本市场服务实体经济,这也是我一向看好A股走牛的重要原因。
 
4、发展高科技和人民币国际化。
 
讲通俗一点,就是:


经济内循环其实就是要过苦日子的另一个代名词。也可以简单理解为好好干活,但不要想着能挣很多钱。在未来很长一段时间里面,物价会相对比较便宜。


 
https://zhuanlan.zhihu.com/p/165347415
 
【Go语言核心36讲:第12节 使用函数的正确姿势(1)】
 
在 Go 语言中,函数可是一等的(first-class)公民,函数类型也是一等的数据类型(函数类型属于引用类型,它的零值为nil)。
这意味着函数不但可以用于封装代码、分割功能、解耦逻辑,还可以化身为普通的值,在其他函数间传递、赋予变量、做类型判断和转换等。“函数是一等的公民”是函数式编程(functional programming)的重要特征。Go 语言在语言层面支持了函数式编程。

package main

import "fmt"

type Printer func(contents string) (n int, err error)

func printToStd(contents string) (bytesNum int, err error) {
return fmt.Println(contents)
}

func main() {
var p Printer
p = printToStd
p("something")
}
函数的签名其实就是函数的参数列表和结果列表的统称,它定义了可用来鉴别不同函数的那些特征,同时也定义了我们与函数交互的方式。


各个参数和结果的名称不能算作函数签名的一部分,对于结果声明,名字也可以没有。且函数的名称也不算函数签名的一部分,只是个标识符而已。


高阶函数
 


1. 接受其他的函数作为参数传入;
2. 把其他的函数作为结果返回。


只要满足了其中任意一个特点,我们就可以说这个函数是一个高阶函数。
 
卫述语句
 
卫述语句是指被用来检查关键的先决条件的合法性,并在检查未通过的情况下立即终止当前代码块执行的语句。在 Go 语言中,if 语句常被作为卫述语句。
if op == nil { 
return 0, errors.New("invalid operation")
}

#每日精进#2020年8月4日

总结zkbhj 发表了文章 • 0 个评论 • 142 次浏览 • 2020-08-04 14:51 • 来自相关话题

【早读:《深入理解计算机系统》】
 
第二章 信息的表示和处理

大多数计算机使用8位的块,或字节,作为最小的可寻址内存单位。

机器级程序将内存视为一个非常大的字节数组,称为虚拟内存。

内存中每个字节由一个唯一的数字来标识,称为它的地址。所有可能的地址的集合就称为虚拟地址空间。

所以,这个虚拟地址空间只是一个展现给机器级程序的概念性映像。实际上,它将DRAM、闪存、磁盘存储器等和操作系统软件结合起来,封装了复杂性,为程序提供一个看上去统一的字节数组。


C语言中的一个指针的值,都是某个存储块的第一个字节的虚拟地址。每个程序对象可以简单地视为一个字节块,而程序本身就是一个字节序列。

十六进制表示法

由于二进制和十进制对于描述位模式来说都非常不方便:二进制太冗长,十进制和位模式的互相转化很麻烦,替代的方法就是引入16进制。

以0x或者0X开头,0~9、A~F,不区分大小写且大小写不敏感。

重要的是二进制、十进制和十六进制之间的互相转换方法,详细的可以进入凯冰科技知识共享中心搜索相关问题或文章查看。

对于x=2的n次方这个公式,转换十六进制,可以转化为n=i+4j,然后得到的十六进制就是:0x + 2的i次方 + j个0,比如512,是2的9次方,9=1+4*2,所以十六进制就是0x200。

字数据大小

每台计算机都有一个字长,指明指针数据的标称大小。字长决定虚拟地址空间的最大大小。32位字长限制的虚拟地址空间位4千兆字节(约4GB),而现在比较普及的64位字长的虚拟空间位16EB。

大多数64位机器也可以运行32位机器编译的程序,这是一种向后兼容。//该编译后的程序可以在32或64位机器上运行
linux> gcc -m32 prog.c

//该编译后,只能在64位机器上运行
linux> gcc -m64 prog.c我们将程序称为32位程序或64位程序,区别在于该程序是如何编译的,而不是其运行的机器类型。

ISO C99引入了确定大小的数据类型,int32_t 和int64_t,其数据大小是固定的,分别为4个字节和8个字节。使用确切大小的整数类型是程序员准确控制数据表示的最佳途径。

程序员应该力图使他们的程序可以在不同的机器和编译器上可移植,可移植的一方面就是说程序对不同数据类型的确切大小不敏感。

比如许多程序员假设一个声明为int类型的程序对象能被用来存储一个指针,这在大多数32位的机器上能够正常工作,但是在一台64位的机器上却会导致问题。所以,1980到2010年期间(32位机器是主流)编写的程序,之后64位机器陆续普及之后,迁移过来的程序就暴露出来许多隐藏的对字长的依赖性问题,导致错误。

【垂直行业如电商如何衡量搜索引擎的优劣】

在电商行业中,无论是2B还是2C,最终的业务目的就是交易成单,众所周知搜索服务旨在让消费者能够更快的定位到自己想要的产品。

一般电商搜索的核心是搜索精度和搜索广度,精度就是搜索的精确性,广度就是搜索结果的范围,其关键结果肯定是“为用户找到想要的商品”,但过于追求搜索的精确度就会导致出现搜索的结果比较少或结果为0的情况,用户搜不到商品势必会引发流失,因此在搜索服务里面还可以做的就是给用户提供一些相关性搜索结果。那么搜索做的好不好,其实就是在搜索精度和搜索广度二者之间做一个比较好的平衡点。

搜索过程中遇到的问题:

1.随机性发现的Bad case
2.KPI或者OKR考核
3.业务方诉求

核心指标
 
“搜索PV”:指访问搜索页面的次数;“搜索UV”:访问过搜索结果页的用户数;“无结果率”:空结果PV/搜索PV,无结果率越低,代表客户搜索需求解决情况越好;“TOP5 PV—CTR”:指该query search结果中,排在前五位的item有被点击的搜索PV/该query搜索PV该指标能一定程度反应排序效果;“人均搜索PV”:搜索PV/搜索UV;该指标的含义比较复杂,一方面人均pv大的话可能代表用户对搜索比较感兴趣,但另一方面人均pv大也可能代表搜索召回的结果较差,导致用户无法使用较少的点击找到满足需求的结果;“有点击搜索PV占比”:有点击搜索PV/搜索PV数;“PV-CTR”:搜索结果页item点击数/搜索PV数;“UV-CTR”:点击的uv / 曝光的uv;“Item-CTR”:搜索结果页item点击数/搜索结果页item总曝光PV数;
 
搜索技术等级分类





 
https://developer.aliyun.com/article/769492
 
  查看全部

【早读:《深入理解计算机系统》】
 
第二章 信息的表示和处理

大多数计算机使用8位的块,或字节,作为最小的可寻址内存单位。

机器级程序将内存视为一个非常大的字节数组,称为虚拟内存。

内存中每个字节由一个唯一的数字来标识,称为它的地址。所有可能的地址的集合就称为虚拟地址空间。

所以,这个虚拟地址空间只是一个展现给机器级程序的概念性映像。实际上,它将DRAM、闪存、磁盘存储器等和操作系统软件结合起来,封装了复杂性,为程序提供一个看上去统一的字节数组。


C语言中的一个指针的值,都是某个存储块的第一个字节的虚拟地址。每个程序对象可以简单地视为一个字节块,而程序本身就是一个字节序列。

十六进制表示法

由于二进制和十进制对于描述位模式来说都非常不方便:二进制太冗长,十进制和位模式的互相转化很麻烦,替代的方法就是引入16进制。

以0x或者0X开头,0~9、A~F,不区分大小写且大小写不敏感。

重要的是二进制、十进制和十六进制之间的互相转换方法,详细的可以进入凯冰科技知识共享中心搜索相关问题或文章查看。

对于x=2的n次方这个公式,转换十六进制,可以转化为n=i+4j,然后得到的十六进制就是:0x + 2的i次方 + j个0,比如512,是2的9次方,9=1+4*2,所以十六进制就是0x200。

字数据大小

每台计算机都有一个字长,指明指针数据的标称大小。字长决定虚拟地址空间的最大大小。32位字长限制的虚拟地址空间位4千兆字节(约4GB),而现在比较普及的64位字长的虚拟空间位16EB。

大多数64位机器也可以运行32位机器编译的程序,这是一种向后兼容。
//该编译后的程序可以在32或64位机器上运行
linux> gcc -m32 prog.c

//该编译后,只能在64位机器上运行
linux> gcc -m64 prog.c
我们将程序称为32位程序或64位程序,区别在于该程序是如何编译的,而不是其运行的机器类型。

ISO C99引入了确定大小的数据类型,int32_t 和int64_t,其数据大小是固定的,分别为4个字节和8个字节。使用确切大小的整数类型是程序员准确控制数据表示的最佳途径。

程序员应该力图使他们的程序可以在不同的机器和编译器上可移植,可移植的一方面就是说程序对不同数据类型的确切大小不敏感。

比如许多程序员假设一个声明为int类型的程序对象能被用来存储一个指针,这在大多数32位的机器上能够正常工作,但是在一台64位的机器上却会导致问题。所以,1980到2010年期间(32位机器是主流)编写的程序,之后64位机器陆续普及之后,迁移过来的程序就暴露出来许多隐藏的对字长的依赖性问题,导致错误。

【垂直行业如电商如何衡量搜索引擎的优劣】

在电商行业中,无论是2B还是2C,最终的业务目的就是交易成单,众所周知搜索服务旨在让消费者能够更快的定位到自己想要的产品。

一般电商搜索的核心是搜索精度和搜索广度,精度就是搜索的精确性,广度就是搜索结果的范围,其关键结果肯定是“为用户找到想要的商品”,但过于追求搜索的精确度就会导致出现搜索的结果比较少或结果为0的情况,用户搜不到商品势必会引发流失,因此在搜索服务里面还可以做的就是给用户提供一些相关性搜索结果。那么搜索做的好不好,其实就是在搜索精度和搜索广度二者之间做一个比较好的平衡点。

搜索过程中遇到的问题:


1.随机性发现的Bad case
2.KPI或者OKR考核
3.业务方诉求


核心指标
 
  • “搜索PV”:指访问搜索页面的次数;
  • “搜索UV”:访问过搜索结果页的用户数;
  • “无结果率”:空结果PV/搜索PV,无结果率越低,代表客户搜索需求解决情况越好;
  • “TOP5 PV—CTR”:指该query search结果中,排在前五位的item有被点击的搜索PV/该query搜索PV该指标能一定程度反应排序效果;
  • “人均搜索PV”:搜索PV/搜索UV;该指标的含义比较复杂,一方面人均pv大的话可能代表用户对搜索比较感兴趣,但另一方面人均pv大也可能代表搜索召回的结果较差,导致用户无法使用较少的点击找到满足需求的结果;
  • “有点击搜索PV占比”:有点击搜索PV/搜索PV数;
  • “PV-CTR”:搜索结果页item点击数/搜索PV数;
  • “UV-CTR”:点击的uv / 曝光的uv;
  • “Item-CTR”:搜索结果页item点击数/搜索结果页item总曝光PV数;

 
搜索技术等级分类

13cfe86b5b2f4aacba814f4fb2f080e5.png

 
https://developer.aliyun.com/article/769492
 
 

#每日精进#2020年8月3日

总结zkbhj 发表了文章 • 0 个评论 • 149 次浏览 • 2020-08-03 20:45 • 来自相关话题

【早读:《深入理解计算机系统》】

第二章 信息的表示和处理

现代计算机存储和处理信息以二值信号表示。二值信号能够很容易的被表示、存储和传输,且用二值信号进行存储和执行计算的电子电路非常简单可靠。

三种重要的数字表示:
 

无符号编码:基于传统的二进制表示法,表示大于或者等于0的数字;
补码编码:表示有符号整数的最常见方式;
浮点数编码:表示实数的科学计数法的以2为基数的版本;


计算机的表示法是以有限数量的位来对一个数字进行编码,所以一旦超出界限,某些运算就会溢出,导致令人吃惊的后果。

整数的计算机运算满足人们所熟知的真正整数运算的许多性质。但是浮点数不一样。

整数的表示虽然只能编码一个相对较小的数值范围,但是这种表示是精确的;

浮点数虽然可以编码一个比较大的数值范围,但是这种标示只是近似的;

通过如下命令,可以在gcc编译C程序时指定C语言版本:
linux> gcc -std=c11 zkbhj.c
//其他版本参数
//GNU 89 无,-std=gnu89
//ANSI ,ISO C90 -ansi,-std=c89
//ISO C99 -std=c99
//ISO C11 -std=c11
【Go核心36讲:第11节 通道的高级玩法】

单向通道

所谓单向通道就是,只能发不能收,或者只能收不能发的通道。

声明一个只能发(向通道发送)不能收(从通道接收),容量为1的单向通道:
var uselessChan = make(chan<- int, 1)声明一个只能收(从通道接收)不能发(向通道发送),容量为1的单向通道:
var uselessChan = make(<-chan int, 1)
与发送操作和接收操作对应,这里的“发”和“收”都是站在操作通道的代码的角度上说的。

单向通道有什么应用价值?

概括地说,单向通道最主要的用途就是约束其他代码的行为。
//参数定义中就约束 ch只能进行发送操作,不能接收
//可以限制方法函数内对参数的操作行为做限定
func SendInt(ch chan<- int) {
ch <- rand.Intn(1000)
}//这段接口声明中,就约定了所以要实现这个接口的实现类型
//都约定了这些方法的参数类型
type Notifier interface {
SendInt(ch chan<- int)
}在实际调用的时候,传递一个双向通道即可,因为Go 语言在这种情况下会自动地把双向通道转换为函数所需的单向通道。

一种专门为了操作通道而存在的语句:select语句

select语句只能与通道联用,它一般由若干个分支组成。每次执行这种语句的时候,一般只有一个分支中的代码会被运行。分支分为两种,一种叫做候选分支,另一种叫做默认分支。每个case表达式中都只能包含操作通道的表达式。
select {
case <-intChannels[0]:
fmt.Println("The first candidate case is selected.")
case <-intChannels[1]:
fmt.Println("The second candidate case is selected.")
case elem := <-intChannels[2]: fmt.Printf("The third candidate case is selected, the element is %d.\n", elem)
default: fmt.Println("No candidate case is selected!")
}select语句只能对其中的每一个case表达式各求值一次。所以,如果我们想连续或定时地操作其中的通道的话,就往往需要通过在for语句中嵌入select语句的方式实现。但这时要注意,简单地在select语句的分支中使用break语句,只能结束当前的select语句的执行,而并不会对外层的for语句产生作用。这种错误的用法可能会让这个for语句无休止地运行下去。

select语句的分支选择规则总结:

1、对于每一个case表达式,都至少会包含一个代表发送操作的发送表达式或者一个代表接收操作的接收表达式;
2、select语句包含的候选分支中的case表达式都会在该语句执行开始时先被求值,并且求值的顺序是依从代码编写的顺序从上到下的;
3、对于每一个case表达式,如果其中的发送表达式或者接收表达式在被求值时,相应的操作正处于阻塞状态,那么对该case表达式的求值就是不成功的;
4、仅当select语句中的所有case表达式都被求值完毕后,它才会开始选择候选分支。这时候,它只会挑选满足选择条件的候选分支执行。所有的都不满足,执行default;
5、如果select语句发现同时有多个候选分支满足选择条件,那么它就会用一种伪随机的算法在这些分支中选择一个并执行;

 
【关于730政治局会议的总结】

15大要点(内循环 + 持久战)
 
​中国发展仍处于战略机遇期从持久战角度认识中长期问题以国内大循环为主题建立中长期协调机制牢牢把握扩大内需这个战略基点确保宏观政策落地见效保持货币供应量合理增长毫不放松抓好常态化疫情防控扩大最终消费加快新基建以新型城镇化带动投资和消费产业链补短板和锻长板从严打击证券违法活动住房不炒缓解疫情对年轻人就业影响

我们遇到的很多问题是中长期的,必须从持久战的角度加以认识

“在泡沫中狂欢的日志不多了,做好潮水退却后的准备,是每个国家,每个人都要面对的现实”

——吴晓灵 前央行副行长 查看全部
【早读:《深入理解计算机系统》】

第二章 信息的表示和处理

现代计算机存储和处理信息以二值信号表示。二值信号能够很容易的被表示、存储和传输,且用二值信号进行存储和执行计算的电子电路非常简单可靠。

三种重要的数字表示:
 


无符号编码:基于传统的二进制表示法,表示大于或者等于0的数字;
补码编码:表示有符号整数的最常见方式;
浮点数编码:表示实数的科学计数法的以2为基数的版本;



计算机的表示法是以有限数量的位来对一个数字进行编码,所以一旦超出界限,某些运算就会溢出,导致令人吃惊的后果。

整数的计算机运算满足人们所熟知的真正整数运算的许多性质。但是浮点数不一样。

整数的表示虽然只能编码一个相对较小的数值范围,但是这种表示是精确的;

浮点数虽然可以编码一个比较大的数值范围,但是这种标示只是近似的;

通过如下命令,可以在gcc编译C程序时指定C语言版本:
linux> gcc -std=c11 zkbhj.c
//其他版本参数
//GNU 89 无,-std=gnu89
//ANSI ,ISO C90 -ansi,-std=c89
//ISO C99 -std=c99
//ISO C11 -std=c11

【Go核心36讲:第11节 通道的高级玩法】

单向通道

所谓单向通道就是,只能发不能收,或者只能收不能发的通道。

声明一个只能发(向通道发送)不能收(从通道接收),容量为1的单向通道:
var uselessChan = make(chan<- int, 1)
声明一个只能收(从通道接收)不能发(向通道发送),容量为1的单向通道:
var uselessChan = make(<-chan int, 1)
与发送操作和接收操作对应,这里的“发”和“收”都是站在操作通道的代码的角度上说的。

单向通道有什么应用价值?

概括地说,单向通道最主要的用途就是约束其他代码的行为。
//参数定义中就约束 ch只能进行发送操作,不能接收
//可以限制方法函数内对参数的操作行为做限定
func SendInt(ch chan<- int) {
ch <- rand.Intn(1000)
}
//这段接口声明中,就约定了所以要实现这个接口的实现类型
//都约定了这些方法的参数类型
type Notifier interface {
SendInt(ch chan<- int)
}
在实际调用的时候,传递一个双向通道即可,因为Go 语言在这种情况下会自动地把双向通道转换为函数所需的单向通道。

一种专门为了操作通道而存在的语句:select语句

select语句只能与通道联用,它一般由若干个分支组成。每次执行这种语句的时候,一般只有一个分支中的代码会被运行。分支分为两种,一种叫做候选分支,另一种叫做默认分支。每个case表达式中都只能包含操作通道的表达式。
select {
case <-intChannels[0]:
fmt.Println("The first candidate case is selected.")
case <-intChannels[1]:
fmt.Println("The second candidate case is selected.")
case elem := <-intChannels[2]: fmt.Printf("The third candidate case is selected, the element is %d.\n", elem)
default: fmt.Println("No candidate case is selected!")
}
select语句只能对其中的每一个case表达式各求值一次。所以,如果我们想连续或定时地操作其中的通道的话,就往往需要通过在for语句中嵌入select语句的方式实现。但这时要注意,简单地在select语句的分支中使用break语句,只能结束当前的select语句的执行,而并不会对外层的for语句产生作用。这种错误的用法可能会让这个for语句无休止地运行下去。

select语句的分支选择规则总结:


1、对于每一个case表达式,都至少会包含一个代表发送操作的发送表达式或者一个代表接收操作的接收表达式;
2、select语句包含的候选分支中的case表达式都会在该语句执行开始时先被求值,并且求值的顺序是依从代码编写的顺序从上到下的;
3、对于每一个case表达式,如果其中的发送表达式或者接收表达式在被求值时,相应的操作正处于阻塞状态,那么对该case表达式的求值就是不成功的;
4、仅当select语句中的所有case表达式都被求值完毕后,它才会开始选择候选分支。这时候,它只会挑选满足选择条件的候选分支执行。所有的都不满足,执行default;
5、如果select语句发现同时有多个候选分支满足选择条件,那么它就会用一种伪随机的算法在这些分支中选择一个并执行;


 
【关于730政治局会议的总结】

15大要点(内循环 + 持久战)
 
  1. ​中国发展仍处于战略机遇期
  2. 从持久战角度认识中长期问题
  3. 以国内大循环为主题
  4. 建立中长期协调机制
  5. 牢牢把握扩大内需这个战略基点
  6. 确保宏观政策落地见效
  7. 保持货币供应量合理增长
  8. 毫不放松抓好常态化疫情防控
  9. 扩大最终消费
  10. 加快新基建
  11. 以新型城镇化带动投资和消费
  12. 产业链补短板和锻长板
  13. 从严打击证券违法活动
  14. 住房不炒
  15. 缓解疫情对年轻人就业影响


我们遇到的很多问题是中长期的,必须从持久战的角度加以认识


“在泡沫中狂欢的日志不多了,做好潮水退却后的准备,是每个国家,每个人都要面对的现实”

——吴晓灵 前央行副行长


#每日精进#2020年08月02日

总结zkbhj 发表了文章 • 0 个评论 • 126 次浏览 • 2020-08-02 19:46 • 来自相关话题

【午读:《深入理解计算机系统》】

第一章 计算机系统漫游

现代系统之间利用网络通信,和其他系统连接在一起。从一个单独的系统来看,网络可以视为一个I/O设备。

系统不仅仅只是硬件,而是硬件和软件互相交织的结合体,他们之间共同协作已达到运行应用程序的最终目的。

Amdahl(安达尔定律)定律

主要思想是:当对系统的某个部分加速时,其对系统整体性能的影响取决于该部分的重要性和加速程度。
主要观点是:想要显著加速整个系统,必须提升全系统中相当大的部分的速度。

 
Amdahl定律描述了改善任何过程的一般原则。

并发和并行

整个计算机发展历史中,我们一直在做两件事:一是让计算机做得更多,二是让计算机运行的更快。
并发:是一个通用概念,指一个同时具有多个活动的系统;
并行:指的是用并发来使一个系统运行得更快。

进程级并发
并发构建在进程整个抽象上,就能够设计出同时有多个程序执行的系统。
单处理器系统:只有一个处理器的系统;
多处理器系统:一个由单操作系统内核控制的多处理器组成的系统。
超线程,有时称为同时多线程,是一项允许一个CPU执行多个控制流的技术。

指令级并行
CPU可以同时执行多条指令的属性称为指令级并行。
超标量处理器:处理器能够达到比一个时钟周期一条指令更快的执行速率。

计算机系统中抽象的重要性
计算机系统提供的一些抽象,它提供不同层次的抽象表示来隐藏实际实现的复杂性。
比如上一节中:文件是对I/O设备的抽象,虚拟内存是对主存和磁盘的抽象,进程则是对一个正在运行的程序的抽象(处理器、主存和I/O的抽象)。
虚拟机,则提供对整个计算机的抽象。

第一章总结
计算机系统是由硬件和系统软件组成的,它们共同协作以运行应用程序。计算机内部的信息被表示为一组组的位,它们根据上下文有不同的解释方式。程序被其他程序翻译成不同的形式,开始是ASCII文本,然后被编译器和链接器翻译成二进制可执行文件。

处理器读取并解释存储在主存里的二进制指令。因为计算机花了大把时间用于存储器、I/O设备和CPU寄存器之间复制数据,所以讲系统中的存储设备划分成层次结构——CPU寄存器在顶部,接着是多层的硬件高速缓存存储器、DRAM主存和磁盘存储器。

在层次模型中,位于更高层的存储设备比低层的存储设备要更快,单位比特造价也更高。层次结构中较高层次存储设备可以作为较低层次存储设备的高速缓存。通过理解和运用这种存储层次结构的知识,程序员可以优化C程序的性能。

操作系统内核是应用程序和硬件之间的媒介。它提供三个基本的抽象:

(1)文件是对I/O设备的抽象

(2)虚拟存储器是对主存和I/O设备的抽象

(3)进程是对处理器、主存和I/O设备的抽象。

另外,虚拟机提供了对整个计算机的抽象。

最后,网络提供了计算机系统之间通信的手段。从特殊系统的角度来看,网络就是一种I/O设备。
 

一周的早读,完成了《深入理解计算机系统》这本又厚又重的计算机最底层原理书籍第一章的阅读和理解。
好好利用碎片时间,以及找到自己的高效时间段及学习方式很重要。
积少成多,积流成河!
明天开启第二章:程序结构和执行!


PS广而告之时间:

正在筹划一个作为技术人员角度的个人分享网站,以很程序员的视角,将学习过的一门语言、一本技术书籍,以“手册”的形式输出出来。
根据能量守恒定律,有输入就要有输出,既然自己有这样的渠道和能力,打算把体系学习过的一些内容,沉淀成一本本技术手册分享出来,可以给更多有同样需求的人提供一些有意义的帮助和指导。
还是凯冰科技10多年来坚持的一句slogan:
代码改变世界,技术改变生活
Code changes the world, technology changes life
doc.zkbhj.com
第一本就是这本《深入理解计算机系统》。嘿嘿~

【关于如何在美团里继续使用支付宝支付的方法】

额。。由于一些你懂的的原因,大部分美团用户已经无法再美团APP上用支付宝进行支付了。这里暂时不评论这件事情的对与错好与坏,只是从技术的角度,帮你找到了一个怎么继续在美团里使用支付宝支付的方法,操作步骤如下:

1、打开美团APP,点击进入个人中心页面,点击右上角的在线客服,进入客服聊天界面;

2、输入问题:支付宝无法使用;

3、在返回的结果里选择:放弃优惠,恢复支付宝。






经过上面的几步“骚操作”,就可以继续在美团里使用支付宝进行支付了!
 
​【Go语言核心36讲:第10节 通道的基本操作 】

☆ 通道(Channel)

通道是Go 语言最有特色的数据类型,是不同Goroutine之间通信的“桥梁”。
 

Don’t communicate by sharing memory; share memory by communicating. (不要通过共享内存来通信,而应该通过通信来共享内存。)


通道类型的值是 Go 语言自带的、唯一一个可以满足并发安全性的类型。

当容量为0时,我们可以称通道为非缓冲通道,也就是不带缓冲的通道。而当容量大于0时,我们可以称为缓冲通道,也就是带有缓冲的通道。

一个通道相当于一个先进先出(FIFO)的队列。也就是说,通道中的各个元素值都是严格地按照发送的顺序排列的,先被发送通道的元素值一定会先被接收。元素值的发送和接收都需要用到操作符<-。我们也可以叫它接送操作符。一个左尖括号紧接着一个减号形象地代表了元素值的传输方向。

☆ 对通道的发送和接收操作都有哪些基本的特性?

1、对于同一个通道,发送操作之间是互斥的,接收操作之间也是互斥的。

元素值从外界进入通道时会被复制。更具体地说,进入通道的并不是在接收操作符右边的那个元素值,而是它的副本。另一方面,元素值从通道进入外界时会被移动。这个移动操作实际上包含了两步,第一步是生成正在通道中的这个元素值的副本,并准备给到接收方,第二步是删除在通道中的这个元素值。

2、发送操作和接收操作中对元素值的处理都是不可分割的。

这里的“不可分割”的意思是,它们处理元素值时都是一气呵成的,绝不会被打断。

发送时,“复制元素值”和“放置副本到通道内部”这两个步骤不会被打断;

接收时,“复制通道内元素值”“放置副本到接收方”“删掉原值”三个步骤不会被打断。

3、发送操作在完全完成之前会被阻塞。接收操作也是如此。

以上各步骤执行期间,其他操作都会被阻塞。如此阻塞代码其实就是为了实现操作的互斥和元素值的完整。

☆ 发送操作和接收操作在什么时候可能被长时间的阻塞?

有缓冲的,如果通道已满,对它的所有发送操作都会被阻塞,直到通道中有元素值被接收走。如果通道已空,对它的所有接收操作都会被阻塞,直到通道中有新的元素值出现。所有被阻塞的goroutine都是按FIFO的策略执行的。

非缓冲通道,情况要简单一些。无论是发送操作还是接收操作,一开始执行就会被阻塞,直到配对的操作也开始执行,才会继续传递。由此可见,非缓冲通道是在用同步的方式传递数据。

对于值为nil的通道,不论它的具体类型是什么,对它的发送操作和接收操作都会永久地处于阻塞状态。它们所属的 goroutine 中的任何代码,都不再会被执行。所以我们一定不要忘记初始化通道!

☆ 发送操作和接收操作在什么时候会引发 panic?

通道一旦关闭,再对它进行发送操作,就会引发 panic。

如果我们试图关闭一个已经关闭了的通道,也会引发 panic。

通过接收表达式的第二个结果值,可以来判断通道是否关闭,但是可能有延时的。即如果通道已经关闭,但还有值未被取出,则这个时候,返回的仍然是true!

所以最佳实践告诉我们,千万不要让接收方关闭通道,而应当让发送方做这件事。 查看全部

【午读:《深入理解计算机系统》】

第一章 计算机系统漫游

现代系统之间利用网络通信,和其他系统连接在一起。从一个单独的系统来看,网络可以视为一个I/O设备。

系统不仅仅只是硬件,而是硬件和软件互相交织的结合体,他们之间共同协作已达到运行应用程序的最终目的。

Amdahl(安达尔定律)定律


主要思想是:当对系统的某个部分加速时,其对系统整体性能的影响取决于该部分的重要性和加速程度。
主要观点是:想要显著加速整个系统,必须提升全系统中相当大的部分的速度。


 
Amdahl定律描述了改善任何过程的一般原则。

并发和并行

整个计算机发展历史中,我们一直在做两件事:一是让计算机做得更多,二是让计算机运行的更快。
并发:是一个通用概念,指一个同时具有多个活动的系统;
并行:指的是用并发来使一个系统运行得更快。

进程级并发
并发构建在进程整个抽象上,就能够设计出同时有多个程序执行的系统。
单处理器系统:只有一个处理器的系统;
多处理器系统:一个由单操作系统内核控制的多处理器组成的系统。
超线程,有时称为同时多线程,是一项允许一个CPU执行多个控制流的技术。

指令级并行
CPU可以同时执行多条指令的属性称为指令级并行。
超标量处理器:处理器能够达到比一个时钟周期一条指令更快的执行速率。

计算机系统中抽象的重要性
计算机系统提供的一些抽象,它提供不同层次的抽象表示来隐藏实际实现的复杂性。
比如上一节中:文件是对I/O设备的抽象,虚拟内存是对主存和磁盘的抽象,进程则是对一个正在运行的程序的抽象(处理器、主存和I/O的抽象)。
虚拟机,则提供对整个计算机的抽象。

第一章总结
计算机系统是由硬件和系统软件组成的,它们共同协作以运行应用程序。计算机内部的信息被表示为一组组的位,它们根据上下文有不同的解释方式。程序被其他程序翻译成不同的形式,开始是ASCII文本,然后被编译器和链接器翻译成二进制可执行文件。

处理器读取并解释存储在主存里的二进制指令。因为计算机花了大把时间用于存储器、I/O设备和CPU寄存器之间复制数据,所以讲系统中的存储设备划分成层次结构——CPU寄存器在顶部,接着是多层的硬件高速缓存存储器、DRAM主存和磁盘存储器。

在层次模型中,位于更高层的存储设备比低层的存储设备要更快,单位比特造价也更高。层次结构中较高层次存储设备可以作为较低层次存储设备的高速缓存。通过理解和运用这种存储层次结构的知识,程序员可以优化C程序的性能。

操作系统内核是应用程序和硬件之间的媒介。它提供三个基本的抽象:

(1)文件是对I/O设备的抽象

(2)虚拟存储器是对主存和I/O设备的抽象

(3)进程是对处理器、主存和I/O设备的抽象。

另外,虚拟机提供了对整个计算机的抽象。

最后,网络提供了计算机系统之间通信的手段。从特殊系统的角度来看,网络就是一种I/O设备。
 


一周的早读,完成了《深入理解计算机系统》这本又厚又重的计算机最底层原理书籍第一章的阅读和理解。
好好利用碎片时间,以及找到自己的高效时间段及学习方式很重要。
积少成多,积流成河!
明天开启第二章:程序结构和执行!



PS广而告之时间

正在筹划一个作为技术人员角度的个人分享网站,以很程序员的视角,将学习过的一门语言、一本技术书籍,以“手册”的形式输出出来。
根据能量守恒定律,有输入就要有输出,既然自己有这样的渠道和能力,打算把体系学习过的一些内容,沉淀成一本本技术手册分享出来,可以给更多有同样需求的人提供一些有意义的帮助和指导。
还是凯冰科技10多年来坚持的一句slogan:
代码改变世界,技术改变生活
Code changes the world, technology changes life
doc.zkbhj.com
第一本就是这本《深入理解计算机系统》。嘿嘿~

【关于如何在美团里继续使用支付宝支付的方法】

额。。由于一些你懂的的原因,大部分美团用户已经无法再美团APP上用支付宝进行支付了。这里暂时不评论这件事情的对与错好与坏,只是从技术的角度,帮你找到了一个怎么继续在美团里使用支付宝支付的方法,操作步骤如下:


1、打开美团APP,点击进入个人中心页面,点击右上角的在线客服,进入客服聊天界面;

2、输入问题:支付宝无法使用;

3、在返回的结果里选择:放弃优惠,恢复支付宝。



WechatIMG147.jpeg

经过上面的几步“骚操作”,就可以继续在美团里使用支付宝进行支付了!
 
​【Go语言核心36讲:第10节 通道的基本操作 】

☆ 通道(Channel)

通道是Go 语言最有特色的数据类型,是不同Goroutine之间通信的“桥梁”。
 


Don’t communicate by sharing memory; share memory by communicating. (不要通过共享内存来通信,而应该通过通信来共享内存。)



通道类型的值是 Go 语言自带的、唯一一个可以满足并发安全性的类型。

当容量为0时,我们可以称通道为非缓冲通道,也就是不带缓冲的通道。而当容量大于0时,我们可以称为缓冲通道,也就是带有缓冲的通道。

一个通道相当于一个先进先出(FIFO)的队列。也就是说,通道中的各个元素值都是严格地按照发送的顺序排列的,先被发送通道的元素值一定会先被接收。元素值的发送和接收都需要用到操作符<-。我们也可以叫它接送操作符。一个左尖括号紧接着一个减号形象地代表了元素值的传输方向。

☆ 对通道的发送和接收操作都有哪些基本的特性?

1、对于同一个通道,发送操作之间是互斥的,接收操作之间也是互斥的。

元素值从外界进入通道时会被复制。更具体地说,进入通道的并不是在接收操作符右边的那个元素值,而是它的副本。另一方面,元素值从通道进入外界时会被移动。这个移动操作实际上包含了两步,第一步是生成正在通道中的这个元素值的副本,并准备给到接收方,第二步是删除在通道中的这个元素值。

2、发送操作和接收操作中对元素值的处理都是不可分割的。

这里的“不可分割”的意思是,它们处理元素值时都是一气呵成的,绝不会被打断。

发送时,“复制元素值”和“放置副本到通道内部”这两个步骤不会被打断;

接收时,“复制通道内元素值”“放置副本到接收方”“删掉原值”三个步骤不会被打断。

3、发送操作在完全完成之前会被阻塞。接收操作也是如此。

以上各步骤执行期间,其他操作都会被阻塞。如此阻塞代码其实就是为了实现操作的互斥和元素值的完整。

☆ 发送操作和接收操作在什么时候可能被长时间的阻塞?

有缓冲的,如果通道已满,对它的所有发送操作都会被阻塞,直到通道中有元素值被接收走。如果通道已空,对它的所有接收操作都会被阻塞,直到通道中有新的元素值出现。所有被阻塞的goroutine都是按FIFO的策略执行的。

非缓冲通道,情况要简单一些。无论是发送操作还是接收操作,一开始执行就会被阻塞,直到配对的操作也开始执行,才会继续传递。由此可见,非缓冲通道是在用同步的方式传递数据。

对于值为nil的通道,不论它的具体类型是什么,对它的发送操作和接收操作都会永久地处于阻塞状态。它们所属的 goroutine 中的任何代码,都不再会被执行。所以我们一定不要忘记初始化通道!

☆ 发送操作和接收操作在什么时候会引发 panic?

通道一旦关闭,再对它进行发送操作,就会引发 panic。

如果我们试图关闭一个已经关闭了的通道,也会引发 panic。

通过接收表达式的第二个结果值,可以来判断通道是否关闭,但是可能有延时的。即如果通道已经关闭,但还有值未被取出,则这个时候,返回的仍然是true!

所以最佳实践告诉我们,千万不要让接收方关闭通道,而应当让发送方做这件事。

中文分词及结巴分词原理概述

搜索推荐zkbhj 发表了文章 • 0 个评论 • 125 次浏览 • 2020-08-01 17:08 • 来自相关话题

中文分词概述

简单来说,中文分词根据实现特点大致可分为两个类别:
 

基于词典的分词方法;
基于统计的分词方法。


基于词典的分词方法

基于词典的分词方法首先会建立一个充分大的词典,然后依据一定的策略扫描句子,若句子中的某个子串与词典中的某个词匹配,则分词成功。

常见的扫描策略有:正向最大匹配、逆向最大匹配、双向最大匹配和最少词数分词。


正向最大匹配

对输入的句子从左至右,以贪心的方式切分出当前位置上长度最大的词,组不了词的字单独划开。其分词原理是:词的颗粒度越大,所能表示的含义越精确。

逆向最大匹配

原理与正向最大匹配相同,但顺序不是从首字开始,而是从末字开始,而且它使用的分词词典是逆序词典,其中每个词条都按逆序方式存放。在实际处理时,先将句子进行倒排处理,生成逆序句子,然后根据逆序词典,对逆序句子用正向最大匹配。

双向最大匹配

将正向最大匹配与逆向最大匹配组合起来,对句子使用这两种方式进行扫描切分,如果两种分词方法得到的匹配结果相同,则认为分词正确,否则,按最小集处理。

最少词数分词

即一句话应该分成数量最少的词串,该方法首先会查找词典中最长的词,看是不是所要分词的句子的子串,如果是则切分,然后不断迭代以上步骤,每次都会在剩余的字符串中取最长的词进行分词,最后就可以得到最少的词数。


总结:基于词典的分词方法简单、速度快,效果也还可以,但对歧义和新词的处理不是很好,对词典中未登录的词没法进行处理。

 
基于统计的分词方法

基于统计的分词方法是从大量已经分词的文本中,利用统计学习方法来学习词的切分规律,从而实现对未知文本的切分。随着大规模语料库的建立,基于统计的分词方法不断受到研究和发展,渐渐成为了主流。

常用的统计学习方法有:隐马尔可夫模型(HMM)、条件随机场(CRF)和基于深度学习的方法。

HMM和CRF

这两种方法实质上是对序列进行标注,将分词问题转化为字的分类问题,每个字有4种词位(类别):词首(B)、词中(M)、词尾(E)和单字成词(S)。由字构词的方法并不依赖于事先编制好的词典,只需对分好词的语料进行训练即可。当模型训练好后,就可对新句子进行预测,预测时会针对每个字生成不同的词位。其中HMM属于生成式模型,CRF属于判别式模型。

基于深度学习的方法

神经网络的序列标注算法在词性标注、命名实体识别等问题上取得了优秀的进展,这些端到端的方法也可以迁移到分词问题上。与所有深度学习的方法一样,该方法需要较大的训练语料才能体现优势,代表为BiLSTM-CRF。

 
总结:基于统计的分词方法能很好地处理歧义和新词问题,效果比基于词典的要好,但该方法需要有大量人工标注分好词的语料作为支撑,训练开销大,就分词速度而言不如前一种。


在实际应用中一般是将词典与统计学习方法结合起来,既发挥词典分词切分速度快的特点,又利用了统计分词结合上下文识别生词、自动消除歧义的优点。结巴分词正是这一类的代表,下面简要介绍一下它的实现算法。
 
结巴分词原理

官方Github上对所用算法的描述为:

基于前缀词典实现高效的词图扫描,生成句子中汉字所有可能成词情况所构成的有向无环图 (DAG);
采用了动态规划查找最大概率路径, 找出基于词频的最大切分组合;
对于未登录词,采用了基于汉字成词能力的 HMM 模型,使用了 Viterbi 算法。


下面逐一介绍:

构造前缀词典

结巴分词首先会依照统计词典dict.txt构造前缀词典。dict.txt含有近35万的词条,每个词条占用一行,其中每一行有3列,第一列为词条,第二列为对应的词频,第三列为词性,构造前缀词典需要用到前两列。

具体做法为:首先定义一个空的python字典,然后遍历dict.txt的每一行,取词条作为字典的键,词频作为对应的键值,然后遍历该词条的前缀,如果前缀对应的键不在字典里,就把该前缀设为字典新的键,对应的键值设为0,如果前缀在字典里,则什么都不做。

这样等遍历完dict.txt后,前缀词典就构造好了。在构造前缀词典时,会对统计词典里所有词条的词频做一下累加,累加值等计算最大概率路径时会用到。

生成有向无环图(DAG)

用正则表达式分割句子后,对每一个单独的子句会生成一个有向无环图。

具体方式为:先定义一个空的python字典,然后遍历子句,当前子句元素的索引会作为字典的一个键,对应的键值为一个python列表(初始为空),然后会以当前索引作为子串的起始索引,不断向后遍历生成不同的子串,如果子串在前缀词典里且键值不为0的话,则把子串的终止索引添加到列表中。

这样等遍历完子句的所有字后,对应的DAG就生成好了。(子串的键值如果是0,则说明它不是一个词条)

计算最大概率路径

DAG的起点到终点会有很多路径,需要找到一条概率最大的路径,然后据此进行分词。可以采用动态规划来求解最大概率路径。

具体来说就是:从子句的最后一个字开始,倒序遍历子句的每个字,取当前字对应索引在DAG字典中的键值(一个python列表),然后遍历该列表,当前字会和列表中每个字两两组合成一个词条,然后基于词频计算出当前字到句尾的概率,以python元组的方式保存最大概率,元祖第一个元素是最大概率的对数,第二个元素为最大概率对应词条的终止索引。

词频可看作DAG中边的权重,之所以取概率的对数是为了防止数值下溢。有了最大概率路径,分词结果也就随之确定。

 对未登录词采用HMM模型进行分词

当出现没有在前缀词典里收录的词时,会采用HMM模型进行分词。HMM模型有5个基本组成:观测序列、状态序列、状态初始概率、状态转移概率和状态发射概率。分词属于HMM的预测问题,即已知观测序列、状态初始概率、状态转移概率和状态发射概率的条件下,求状态序列。结巴分词已经内置了训练好的状态初始概率、状态转移概率和状态发射概率。

句子会作为观测序列,当有新句子进来时,具体做法为:先通过Viterbi算法求出概率最大的状态序列,然后基于状态序列输出分词结果(每个字的状态为B、M、E、S之一)。

至此,结巴分词的原理就简单介绍完了。
 
参考文档:
https://www.cnblogs.com/cyandn/p/10891608.html
https://www.cnblogs.com/zhbzz2007/p/6084196.html
  查看全部
中文分词概述

简单来说,中文分词根据实现特点大致可分为两个类别:
 


基于词典的分词方法;
基于统计的分词方法。



基于词典的分词方法

基于词典的分词方法首先会建立一个充分大的词典,然后依据一定的策略扫描句子,若句子中的某个子串与词典中的某个词匹配,则分词成功。

常见的扫描策略有:正向最大匹配、逆向最大匹配、双向最大匹配和最少词数分词。


正向最大匹配

对输入的句子从左至右,以贪心的方式切分出当前位置上长度最大的词,组不了词的字单独划开。其分词原理是:词的颗粒度越大,所能表示的含义越精确。

逆向最大匹配

原理与正向最大匹配相同,但顺序不是从首字开始,而是从末字开始,而且它使用的分词词典是逆序词典,其中每个词条都按逆序方式存放。在实际处理时,先将句子进行倒排处理,生成逆序句子,然后根据逆序词典,对逆序句子用正向最大匹配。

双向最大匹配

将正向最大匹配与逆向最大匹配组合起来,对句子使用这两种方式进行扫描切分,如果两种分词方法得到的匹配结果相同,则认为分词正确,否则,按最小集处理。

最少词数分词

即一句话应该分成数量最少的词串,该方法首先会查找词典中最长的词,看是不是所要分词的句子的子串,如果是则切分,然后不断迭代以上步骤,每次都会在剩余的字符串中取最长的词进行分词,最后就可以得到最少的词数。


总结:基于词典的分词方法简单、速度快,效果也还可以,但对歧义和新词的处理不是很好,对词典中未登录的词没法进行处理。

 
基于统计的分词方法

基于统计的分词方法是从大量已经分词的文本中,利用统计学习方法来学习词的切分规律,从而实现对未知文本的切分。随着大规模语料库的建立,基于统计的分词方法不断受到研究和发展,渐渐成为了主流。

常用的统计学习方法有:隐马尔可夫模型(HMM)、条件随机场(CRF)和基于深度学习的方法。

HMM和CRF

这两种方法实质上是对序列进行标注,将分词问题转化为字的分类问题,每个字有4种词位(类别):词首(B)、词中(M)、词尾(E)和单字成词(S)。由字构词的方法并不依赖于事先编制好的词典,只需对分好词的语料进行训练即可。当模型训练好后,就可对新句子进行预测,预测时会针对每个字生成不同的词位。其中HMM属于生成式模型,CRF属于判别式模型。

基于深度学习的方法

神经网络的序列标注算法在词性标注、命名实体识别等问题上取得了优秀的进展,这些端到端的方法也可以迁移到分词问题上。与所有深度学习的方法一样,该方法需要较大的训练语料才能体现优势,代表为BiLSTM-CRF。

 
总结:基于统计的分词方法能很好地处理歧义和新词问题,效果比基于词典的要好,但该方法需要有大量人工标注分好词的语料作为支撑,训练开销大,就分词速度而言不如前一种。


在实际应用中一般是将词典与统计学习方法结合起来,既发挥词典分词切分速度快的特点,又利用了统计分词结合上下文识别生词、自动消除歧义的优点。结巴分词正是这一类的代表,下面简要介绍一下它的实现算法。
 
结巴分词原理

官方Github上对所用算法的描述为:


基于前缀词典实现高效的词图扫描,生成句子中汉字所有可能成词情况所构成的有向无环图 (DAG)
采用了动态规划查找最大概率路径, 找出基于词频的最大切分组合;
对于未登录词,采用了基于汉字成词能力的 HMM 模型,使用了 Viterbi 算法



下面逐一介绍:

构造前缀词典

结巴分词首先会依照统计词典dict.txt构造前缀词典。dict.txt含有近35万的词条,每个词条占用一行,其中每一行有3列,第一列为词条,第二列为对应的词频,第三列为词性,构造前缀词典需要用到前两列。

具体做法为:首先定义一个空的python字典,然后遍历dict.txt的每一行,取词条作为字典的键,词频作为对应的键值,然后遍历该词条的前缀,如果前缀对应的键不在字典里,就把该前缀设为字典新的键,对应的键值设为0,如果前缀在字典里,则什么都不做。

这样等遍历完dict.txt后,前缀词典就构造好了。在构造前缀词典时,会对统计词典里所有词条的词频做一下累加,累加值等计算最大概率路径时会用到。

生成有向无环图(DAG)

用正则表达式分割句子后,对每一个单独的子句会生成一个有向无环图。

具体方式为:先定义一个空的python字典,然后遍历子句,当前子句元素的索引会作为字典的一个键,对应的键值为一个python列表(初始为空),然后会以当前索引作为子串的起始索引,不断向后遍历生成不同的子串,如果子串在前缀词典里且键值不为0的话,则把子串的终止索引添加到列表中。

这样等遍历完子句的所有字后,对应的DAG就生成好了。(子串的键值如果是0,则说明它不是一个词条)

计算最大概率路径

DAG的起点到终点会有很多路径,需要找到一条概率最大的路径,然后据此进行分词。可以采用动态规划来求解最大概率路径。

具体来说就是:从子句的最后一个字开始,倒序遍历子句的每个字,取当前字对应索引在DAG字典中的键值(一个python列表),然后遍历该列表,当前字会和列表中每个字两两组合成一个词条,然后基于词频计算出当前字到句尾的概率,以python元组的方式保存最大概率,元祖第一个元素是最大概率的对数,第二个元素为最大概率对应词条的终止索引。

词频可看作DAG中边的权重,之所以取概率的对数是为了防止数值下溢。有了最大概率路径,分词结果也就随之确定。

 对未登录词采用HMM模型进行分词

当出现没有在前缀词典里收录的词时,会采用HMM模型进行分词。HMM模型有5个基本组成:观测序列、状态序列、状态初始概率、状态转移概率和状态发射概率。分词属于HMM的预测问题,即已知观测序列、状态初始概率、状态转移概率和状态发射概率的条件下,求状态序列。结巴分词已经内置了训练好的状态初始概率、状态转移概率和状态发射概率。

句子会作为观测序列,当有新句子进来时,具体做法为:先通过Viterbi算法求出概率最大的状态序列,然后基于状态序列输出分词结果(每个字的状态为B、M、E、S之一)。

至此,结巴分词的原理就简单介绍完了。
 
参考文档:
https://www.cnblogs.com/cyandn/p/10891608.html
https://www.cnblogs.com/zhbzz2007/p/6084196.html