2023年3月

简介

命令模式(Command Pattern)是一种数据驱动的设计模式,也是一种行为型设计模式。这种模式的请求以命令的形式包裹在对象中,并传给调用对象。调用对象再寻找合适的对象,并把该命令传给相应的处理者。即把请求或操作封装成单个对象,并使其可以被参数化和延迟执行,这种方式将命令和执行者进行了有效解耦。

如果你需要通过操作来参数化对象,可使用命令模式。如果你想要将操作放入队列中、操作的执行或者远程执行操作, 可使用命令模式。如果你想要实现操作回滚功能,可使用命令模式。

作用

  1. 将不同命令按照抽象命令封装成不同的对象,将这些命令放到调用者里。
  2. 客户通过调用者执行命令再去调用接受者的动作,顺序为:客户调用方->调用者->命令对象->接受者。
  3. 同其他对象一样,命令也可以实现序列化,从而方便地写入文件或数据库中,实现延迟执行。

实现步骤

  1. 创建一个抽象命令接口,实现基本的命令方法。
  2. 创建多个具体命令类,实现抽象命令接口,以来命令接收者。
  3. 创建命令接收者,也就是具体业务类,接受命令并执行动作。
  4. 创建命令调用者,这是一个聚合命令的类,添加命令和执行命令。

UML

Java代码

基础命令接口

//Command.java 命令抽象接口
public interfaceCommand {voidexecute();
}

具体命令类,可以多个命令

//BuyCommand.java 购买命令,操作receiver,实现了抽象命令类
public class BuyCommand implementsCommand {privateStockReceiver stockReceiver;publicBuyCommand(StockReceiver stockReceiver) {this.stockReceiver =stockReceiver;
}
//命令类调用执行者的实际动作 public voidexecute() {
System.out.println(
this.getClass().getName() + "::execute() ");this.stockReceiver.buy();
}
}
//SellCommand.java 出售命令,操作receiver,实现了抽象命令类 public class SellCommand implementsCommand {privateStockReceiver stockReceiver;publicSellCommand(StockReceiver stockReceiver) {this.stockReceiver =stockReceiver;
}
//命令类调用执行者的实际动作 public voidexecute() {
System.out.println(
this.getClass().getName() + "::execute() ");
stockReceiver.sell();
}
}

命令调用类

//CommandInvoker.java 命令调用类,通过关联命令来执行命令的调用
public classCommandInvoker {private List<Command> commandList = new ArrayList<Command>();//储存命令
    public voidtakeOrder(Command command) {
System.out.println(
this.getClass().getName() + "::takeOrder() " +command.getClass().getName());
commandList.add(command);
}
//统一执行 public voidexecuteOrders() {
System.out.println(
this.getClass().getName() + "::executeOrders() ");for(Command command : commandList) {
command.execute();
}
commandList.clear();
}
}

命令接收执行类

//StockReceiver.java 命令模式真正的执行类,不直接对外,通过command来调用
public classStockReceiver {privateString name;private intnum;public StockReceiver(String name, intnum) {this.name =name;this.num =num;
}
public voidbuy() {
System.out.println(
this.getClass().getName() + "::buy() [name=" + this.name + " num=" + this.num + "]");
}
public voidsell() {
System.out.println(
this.getClass().getName() + "::sell() [name=" + this.name + " num=" + this.num + "]");
}
public voidsetName(String name) {this.setName(name);
}
publicString getName() {return this.name;
}
public void setNum(intnum) {this.num =num;
}
public intgetNum() {return this.num;
}
}

测试调用

    /** 命令模式是客户端通过一个命令执行者invoker,去执行某个命令command。
* 而命令则调用了业务类receiver的具体动作,从而完成真正的执行。
* 这种方式将命令和执行者进行了有效解耦。
*/ //先声明一个被操作对象,也就是接收者 StockReceiver stock1 = new StockReceiver("Apple", 200);//再声明具体的命令 BuyCommand buyCommand = newBuyCommand(stock1);
SellCommand sellCommand
= newSellCommand(stock1);//最后声明调用者,由调用者来执行具体命令 CommandInvoker invoker = newCommandInvoker();
invoker.takeOrder(buyCommand);
invoker.takeOrder(sellCommand);
invoker.executeOrders();
//再执行一只股票 StockReceiver stock2 = new StockReceiver("Google", 100);
BuyCommand buyCommand2
= newBuyCommand(stock2);
invoker.takeOrder(buyCommand2);
invoker.executeOrders();

Go代码

基础命令接口

//Command.go 命令抽象接口
type Command interface{
GetName()
stringSetStockReceiver(stockReceiver*StockReceiver)
Execute()
}

具体命令类,可以多个命令

//BuyCommand.go 购买命令,操作receiver,实现了抽象命令类
type BuyCommand struct{
Name
string `default:"BuyCommand"`
stockReceiver
*StockReceiver
}
func (c *BuyCommand) GetName() string{returnc.Name
}
func (c *BuyCommand) SetStockReceiver(stockReceiver *StockReceiver) {
c.stockReceiver
=stockReceiver
}
//命令类调用执行者来自行真正的动作 func (c *BuyCommand) Execute() {
fmt.Println(
"BuyCommand::Execute()")
c.stockReceiver.Buy()
}
//SellCommand.go 出售命令,操作receiver,实现了抽象命令类 type SellCommand struct{
Name
string `default:"BuyCommand"`
stockReceiver
*StockReceiver
}
func (s *SellCommand) GetName() string{returns.Name
}
func (s *SellCommand) SetStockReceiver(stockReceiver *StockReceiver) {
s.stockReceiver
=stockReceiver
}
//命令类调用执行者来自行真正的动作 func (s *SellCommand) Execute() {
fmt.Println(
"SellCommand::Execute()")
s.stockReceiver.Sell()
}

命令调用类

//CommandInvoker.go 命令调用类,通过关联命令来执行命令的调用
type CommandInvoker struct{
Name
stringcommandList []Command
}
func (c *CommandInvoker) GetName() string{returnc.Name
}
//储存命令 func (c *CommandInvoker) TakeOrder(command Command) {
fmt.Println(
"CommandInvoker::TakeOrder()" +command.GetName())
c.commandList
= append(c.commandList, command)
}
//统一执行 func (c *CommandInvoker) ExecuteOrders() {
fmt.Println(
"CommandInvoker::ExecuteOrders()")for _, command := rangec.commandList {
command.Execute()
}
//命令执行后清除 c.commandList = c.commandList[:0]
}

命令接收执行类

//StockReceiver.go 命令模式真正的执行类,不直接对外,通过command来调用
type StockReceiver struct{
Name
stringNumint}func (s *StockReceiver) Buy() {
fmt.Println(
"StockReceiver::Buy() [Name=" +s.Name+ "Num=" + strconv.Itoa(s.Num) + "]")
}
func (s *StockReceiver) Sell() {
fmt.Println(
"StockReceiver::Sell() [Name=" +s.Name+ "Num=" + strconv.Itoa(s.Num) + "]")
}

测试调用

//main包下的main入口方法
funcmain() {
fmt.Println(
"test start:")/** 命令模式是客户端通过一个命令执行者invoker,去执行某个命令command
* 而命令则调用了业务类receiver的具体动作,从而完成真正的执行
* 这种方式将命令和执行者进行了有效解耦。
*/ //先声明一个被操作对象,也就是接收者 var stock1 = &src.StockReceiver{
Name:
"Apple",
Num:
200,
}
//再声明具体的命令 var buyCommand = &src.BuyCommand{
Name:
"buyCommand",
}
buyCommand.SetStockReceiver(stock1)
var sellCommand = &src.SellCommand{
Name:
"sellCommand",
}
sellCommand.SetStockReceiver(stock1)
//最后声明调用者,由调用者来执行具体命令 var invoker = &src.CommandInvoker{
Name:
"invoker",
}
invoker.TakeOrder(buyCommand)
invoker.TakeOrder(sellCommand)
invoker.ExecuteOrders()
//再执行一只股票 var stock2 = &src.StockReceiver{
Name:
"Google",
Num:
100,
}
var buyCommand2 = &src.BuyCommand{
Name:
"buyCommand2",
}
buyCommand2.SetStockReceiver(stock2)
invoker.TakeOrder(buyCommand2)
invoker.ExecuteOrders()
}

C语言代码

基础对象定义

//func.h文件,基础命令结构体head
#include <stdio.h>#include<stdlib.h>#include<stdbool.h>#include<string.h>

//基础命令结构体
typedef structCommand
{
char name[50];struct StockReceiver *stock_receiver;void (*set_stock_receiver)(struct Command *command, struct StockReceiver *);void (*execute)(struct Command *);
} Command;
//接受者对象 typedef structStockReceiver
{
char name[50];intnum;void (*buy)(struct StockReceiver *);void (*sell)(struct StockReceiver *);
} StockReceiver;
StockReceiver
*stock_receiver_constructor(char *name, intnum);//继承命令结构体 typedef structBuyCommand
{
char name[50];struct StockReceiver *stock_receiver;void (*set_stock_receiver)(struct BuyCommand *command, struct StockReceiver *);void (*execute)(struct Command *);
} BuyCommand;
BuyCommand
*buy_command_constructor(char *name);//继承命令结构体 typedef structSellCommand
{
char name[50];struct StockReceiver *stock_receiver;void (*set_stock_receiver)(struct SellCommand *command, struct StockReceiver *);void (*execute)(struct Command *);
} SellCommand;
SellCommand
*sell_command_constructor(char *name);//命令执行者 typedef structCommandInvoker
{
char name[50];void (*take_order)(struct CommandInvoker *invoker, Command *command);void (*execute_orders)(struct CommandInvoker *invoker);//数组命令列表,记录待执行的命令对象 struct Command **command_list;//数组长度记录 intcommand_list_size;//若是柔性数组,则放在结构体最后,可以动态追加内容//struct Command *command_list[]; } CommandInvoker;
CommandInvoker
*command_invoker_constructor(char *name);

具体命令类,可以多个命令

//buy_command.c 购买命令,操作receiver,实现了抽象命令类
#include "func.h"

//购买命令,操作receiver,实现了抽象命令类

void set_buy_stock_receiver(BuyCommand *command, StockReceiver *receiver)
{
command
->stock_receiver =receiver;
}
//命令类调用执行者来自行真正的动作 void buy_command_execute(Command *command)
{
printf(
"\r\n BuyCommand::execute() [command->name=%s]", command->name);
command
->stock_receiver->buy(command->stock_receiver);
}
//创建Buy命令对象 BuyCommand *buy_command_constructor(char *name)
{
Command
*command = (Command *)malloc(sizeof(Command));
strncpy(command
->name, name, 50);
command
->execute = &buy_command_execute;//转为BuyCommand BuyCommand *buy_command = (BuyCommand *)command;
buy_command
->set_stock_receiver = &set_buy_stock_receiver;returnbuy_command;
}
//sell_command.c 出售命令,操作receiver,实现了抽象命令类 #include "func.h" //出售命令,操作receiver,实现了抽象命令类 void set_sell_stock_receiver(SellCommand *command, StockReceiver *receiver) {
command
->stock_receiver =receiver;
}
//命令类调用执行者来自行真正的动作 void sell_command_execute(Command *command) {
printf(
"\r\n SellCommand::execute() [command->name=%s]", command->name);
command
->stock_receiver->sell(command->stock_receiver);
}
//创建Sell命令对象 SellCommand *sell_command_constructor(char *name)
{
Command
*command = (Command *)malloc(sizeof(Command));
strncpy(command
->name, name, 50);
command
->execute = &sell_command_execute;//转为SellCommand SellCommand *buy_command = (SellCommand *)command;
buy_command
->set_stock_receiver = &set_sell_stock_receiver;returnbuy_command;
}

命令调用类

//command_invoker.c 命令调用类,通过关联命令来执行命令的调用
#include "func.h"

/*命令调用类,通过关联命令来实行命令的调用
在命令模式中,Invoker(调用者)是一个可选的组件,
它负责将Command对象传递给Receiver,
并调用Command对象的execute方法来执行命令。
Invoker在实现命令模式时可以有多种实现方式。
*/ void print_command_list(Command **list, intcommand_list_size)
{
printf(
"\r\nThe current command_list:");for (int i = 0; i < command_list_size; i++)
{
printf(
"\r\n [i=%d, command->name=%s]", i, list[i]->name);
}
}
//把命令存储到调用者的命令列表 void invoker_take_order(CommandInvoker *invoker, Command *command)
{
printf(
"\r\n CommandInvoker::take_order() [invoker->name=%s, command->name=%s, invoker->command_list_size=%d]", invoker->name, command->name, invoker->command_list_size);//列表长度增加1位 int new_command_list_size = invoker->command_list_size + 1;/*如果采取柔性数组,则无需申请新空间和复制内容*/ //把原列表命令暂存下来 Command **old_command_list = invoker->command_list;//给命令列表申请新空间 invoker->command_list = (Command **)calloc(new_command_list_size, sizeof(Command *));//复制原有命令到命令列表,如果采取柔性数组则无需复制 for (int i = 0; i < invoker->command_list_size; i++)
{
invoker
->command_list[i] =old_command_list[i];
}
free(old_command_list);//把新的命令添加列表最后 invoker->command_list[new_command_list_size - 1] =command;
invoker
->command_list_size =new_command_list_size;//打印当前有多少命令//print_command_list(invoker->command_list, invoker->command_list_size); }//统一执行全部命令 void invoker_execute_orders(CommandInvoker *invoker)
{
printf(
"\r\n CommandInvoker::execute_orders()");int command_list_size = invoker->command_list_size;
Command
**command_list = invoker->command_list;for (int i = 0; i < command_list_size; i++)
{
Command
*command =command_list[i];
command
->execute(command);
command_list[i]
=NULL;
}
//命令执行完后清除命令列表 invoker->command_list_size = 0;
invoker
->command_list = (Command **)calloc(0, sizeof(Command *));
}
//初始化CommandInvoker命令对象 CommandInvoker *command_invoker_constructor(char *name)
{
printf(
"\r\n command_invoker_constructor() [name=%s]", name);
CommandInvoker
*invoker = (CommandInvoker *)malloc(sizeof(CommandInvoker));
strncpy(invoker
->name, name, 50);
invoker
->command_list_size = 0;
invoker
->take_order = &invoker_take_order;
invoker
->execute_orders = &invoker_execute_orders;returninvoker;
}

命令接收执行类

//stock_receiver.c 命令模式真正的执行类,不直接对外,通过command来调用
#include "func.h"

/*命令模式真正的执行类,不直接对外,通过command来调用*/

void stock_receiver_buy(StockReceiver *stock_receiver) {
printf(
"\r\n StockReceiver::buy() [name=%s num=%d]", stock_receiver->name, stock_receiver->num);
}
void stock_receiver_sell(StockReceiver *stock_receiver) {
printf(
"\r\n StockReceiver::sell() [name=%s num=%d]", stock_receiver->name, stock_receiver->num);
}
//创建StockReceiver命令对象 StockReceiver *stock_receiver_constructor(char *name, intnum)
{
printf(
"\r\n stock_receiver_constructor() [name=%s, num=%d]", name, num);
StockReceiver
*receiver = (StockReceiver *)malloc(sizeof(StockReceiver));
strncpy(receiver
->name, name, 50);
receiver
->num =num;
receiver
->buy = &stock_receiver_buy;
receiver
->sell = &stock_receiver_sell;returnreceiver;
}

测试调用

#include "../src/func.h"

int main(void)
{
printf(
"test start:\r\n");/** 命令模式是一种行为设计模式,它将请求或操作封装成单个对象,并使其可以被参数化和延迟执行。
* 在命令模式中,客户端通过一个命令执行者invoker,去执行某个命令command
* 而命令则调用了业务类receiver的具体动作,从而完成真正的执行
* 这种方式将命令和执行者进行了有效解耦。
*/ //先声明一个被操作对象,也就是接收者 StockReceiver *stocker_receiver1 = stock_receiver_constructor("Apple", 200);//再声明具体的命令 BuyCommand *buy_command = buy_command_constructor("buy_command");
buy_command
->set_stock_receiver(buy_command, stocker_receiver1);

SellCommand
*sell_command = sell_command_constructor("sell_command");
sell_command
->set_stock_receiver(sell_command, stocker_receiver1);//最后声明调用者,由调用者来执行具体命令 CommandInvoker *invoker = command_invoker_constructor("invoker");
invoker
->take_order(invoker, (Command *)buy_command);
invoker
->take_order(invoker, (Command *)sell_command);
invoker
->execute_orders(invoker);//再执行一只股票,声明新的接受者 StockReceiver *stock_receiver2 = stock_receiver_constructor("Google", 100);
BuyCommand
*buy_command2 = buy_command_constructor("buy_command2");//这次只有buy命令 buy_command2->set_stock_receiver(buy_command2, stock_receiver2);//还用原来的invoker,或者新建invoker invoker->take_order(invoker, (Command *)buy_command2);
invoker
->execute_orders(invoker);return 0;
}

更多语言版本

不同语言实现设计模式源码:
https://github.com/microwind/design-pattern

由于硬盘和内存的造价差异,一台主机实例的硬盘容量通常会远超于内存容量。对于数据库等应用而言,为了保证更快的查询效率,通常会将使用过的数据放在内存中进行加速读取。

数据页与索引页的LRU

数据页和索引页的目的在于缓存一部分的表数据和索引数据,其数据总量通常会超过缓冲池大小,所以缓冲池中应只缓冲那些经常使用的热点数据。InnoDB内存管理使用的是最近最少使用(Least Recently Used, LRU)算法。来淘汰最久未使用的数据

在一般的LRU算法中,当链表中的某一个数据被读取时,将会将其放置于队首。当新增数据且链表已达最大数量时,将链表尾部的数据移除,并将新增的数据置于链表首部。

InnoDB的LRU并没有使用传统的双端链表,而是做了改进,这里有两个问题:

  • 预读失效
  • 缓冲池污染

优化预读失效

由于预读(Read-Ahead),提前把页放入了缓冲池,但最终 MySQL 并没有从页中读取数据,称为预读失效。

Read-Ahead机制

Read-Ahead用于异步预取buffer pool中的多个page的一个预测行为。

InnoDB使用两种提前预读Read-Ahead算法来提高I/O性能。

  • Linear read-ahead 线性预读

如果一个extent中的被顺序读取的page超过或者等于   innodb_read_ahead_threshold  参数变量时,Innodb将会异步的将下一个extent读取到buffer pool中,innodb_read_ahead_threshold可以设置为0-64的任何值(注:innodb中每个extent就只有64个page),默认为56。值越大,访问模式检查就越严格。

  • Random read-ahead 随机预读

如果当同一个extent中连续的13个page在buffer pool中发现时,Innodb会将该extent中的剩余page读到buffer pool中。控制参数  innodb_random_read_ahead  默认没有开启。

如何对预读失效进行优化?

要优化预读失效,思路是:

  • 让预读失败的页,停留在缓冲池LRU里的时间尽可能短
  • 让真正被读取的页,才挪到缓冲池LRU的头部

InnoDB 的具体解决方法

由上图可以看出 InnoDB 将 LRU List 分为两部分,默认前 5/8 为 New Sublist(新生代)用于存储经常被使用的热点数据页,后 3/8 为 Old Sublist(老生代),新读入的数据页默认被放到 Old Sublist 中,只有满足一定条件后,才会被移入 New Sublist。

新生代和老生代代比例在 MySQL 中通过参数 innodb_old_blocks_pct 控制,值的范围是5到95.默认值是37(即池的3/8)。

  • 如果数据页真正被读取(预读成功),才会加入到新生代的头部
  • 如果数据页没有被读取,则会比新生代里的“热数据页”更早被淘汰出缓冲池

举个例子,整个缓冲池如图

假如有一个页号为 50 的数据页页被预读加入缓冲池:

(a). 页号为50 的数据页只会从老生代头部插入,老生代尾部(也是整体尾部)的页会被淘汰掉,即 8 号数据页被淘汰。

(b). 假如页号为50 的数据页不被真正读取,即预读失败,它将比新生代的数据更早淘汰出缓冲池

(c). 假如 50 这一页立刻被读取到,例如SQL访问了页内的行row数据。它会被立刻加入到新生代的头部,同时新生代的页会被挤到老生代,此时并不会有页面被真正淘汰

改进版缓冲池LRU能够很好的解决“预读失败”的问题。但仍然无法解决缓冲池被污染但问题。

缓冲池污染

当某一个SQL语句,要批量扫描大量数据时,可能导致把缓冲池的所有页都替换出去,导致大量热数据被换出,MySQL 性能急剧下降,这种情况叫缓冲池污染。

解决方法

缓冲池加入了一个“老生代停留时间窗口”的机制:

(a). 假设T=老生代停留时间窗口

(b). 插入老生代头部的页,即使立刻被访问,并不会立刻放入新生代头部

(c). 只有满足“被访问”并且“在老生代停留时间”大于T,才会被放入新生代头部

假如批量数据扫描,有91、92、93、94、95、96、97、98、99等页面将要依次被访问

如果没有“老生代停留时间窗口”的策略,这些批量被访问的页面,会置换出大量热数据。

加入“老生代停留时间窗口”策略后,短时间内被大量加载的页,并不会立刻插入新生代头部,而是优先淘汰那些,短期内仅仅访问了一次的页。

只有在老生代呆的时间足够久,停留时间大于T,才会被插入新生代头部。

老生代的停留时间由参数
innodb_old_blocks_time
控制,单位为毫秒,默认是1000

总结

  1. 缓冲池(buffer pool)是一种常见的降低磁盘访问的机制
  2. InnoDB的缓冲池以数据页(page)为单位缓存数据
  3. InnoDB 对普通 LRU 进行了优化,
  • 将缓冲池分为老生代和新生代,入缓冲池的页,优先进入老生代,页被访问,才进入新生代,以解决预读失效的问题。
  • 同时采用老生代停留时间窗口机制,当数据页被访问且在老生代停留时间超过配置阈值的,才进入新生代,以解决批量数据访问,大量热数据淘汰的问题

现在很多公司都在做或者计划做研发效能,也知道研发效能工作很重要,能提高产研运同学的协同效率,提高员工的工作效率和质量,提高业务交付效率和交付质量,但是价值有多大?效率又有多高呢?因为不容易说清楚,所以经常碰到一些质疑和灵魂拷问。

  • 如何衡量研发效能的效果?

  • 如何衡量研发效能的作用?

  • 如何说清楚研发效能工作的价值?

  • 研发效能是做啥的?有啥用?有多大用?

研发效能定义

之前我给过研发效能的定义,但是随着这个领域的发展,大家越来越注重「开发者体验」,因为这项工作太重要了,对员工的工作效率的确影响很大。之前我们做研发效能平台的时候就特别注重开发者体验,但对于有些公司还停留在工具有无的阶段,暂时注意不到这块。所以这次我对研发效能的定义进行了优化,想以此引起大家对这块的注意,促进这块的发展,形成共识。研发效能定义如下:

研发效能是一个组织高效交付产品的能力,以及围绕提高这一能力所建立起来的由规范、流程、工具、度量体系、实践等组成的系统工程体系。目标是优化开发者体验,夯实产品研发运营基础设施和赋能组织持续高质高效地交付产品价值。

研发效能主要工作

  • 规范制定:制
    定产研运协同的规范

  • 流程梳理:梳理产研运协同的流程

  • 平台建设:建设支持产研运协同的基础平台

  • 平台运营和服务:对产研运提供服务,并进行平台运营

  • 效能度量:对产研运协同进行效能度量,分析存在的问题并推动改进和优化

研发效能工作目标细分

  • 规范制
    定和技术治理

    • 梳理公司技术现状、制定技术治理方向

    • 协调制定技术选型、研发流程等技术类规范

    • 解决公司业务发展过程中遇到的共性问题和技术挑战

    • 为不同业务场景提供全面的技术解决方案

    • 进行规章制度、规范、平台使用的宣传、培训、布道、配套工具推广等

  • 推动建设和优化产研运协作流程

    • 梳理和优化产研运之间协作的流程

    • 推动产研运高效协作

    • 梳理、宣导和推广工程

      佳实践

  • 研发效能平台建设



    • 佳实践固化到平台,进行研发效能平台建设

    • 保证
      效能平台的稳定性、可用性

    • 效能平台功能完备的同时保持高度易用

    • 高效率完成
      效能平台上的高频操作

  • 研发效能平台运营和服务

    • 及时响应研发效能平台用户的日常诉求,高效解决用户问题

    • 及时收集、梳理和提炼用户的诉求,进行痛点分析

    • 通过产品运营、内容运营、活动运营、用户运营,让用户更多地了解我们的平台,,让平台「有人用、会用、善用」

  • 研发效能度量

    • 梳理、计算、展示和分析衡量端到端尽


      快交付效率的指标

    • 梳理、计算、展示和分析衡量端到端高质量交付的指标

    • 梳理、计算、展示和分析衡量卓越工程能力、持续交付能力的指标

    • 通过研发效能度量发现产研运效能问题,推动组织解决、改进和优化

研发效能
价值

说清楚了研发效能的具体工作,是不是就很容易说清楚研发效能的价值了?不是的。讲清楚了研发效能的具体工作,只是让大家了解了研发效能是什么,具体做什么,这对一线同学很容易讲清楚,但是对于
往上+1/+2的领导来说还不是很容易get 到点子上,你讲了这么多,在他们看来是抓不到点子上。因为对于公司来说,团队带来的价值无非两件事,要么收入,要么成本,简单点说你给公司带来多少收入,或者你节约了多少成本。

说价值就要提收入和成本,但这对研发效能却不是一件容易说清楚的事情。为什么业务的价值容易讲清楚?我用多少人开发的功能给公司带来多少利润,这是非常容易衡量的,只要每个月让财务出个数据就好。对于大多数公司来说,1)研发效能团队不对外,也就是无法直接给公司创造收入。2)研发效能工作涉及面广,见效慢,需要长期投入,建设初期很难算清帮公司省了多少钱,甚至还要有一定的人力成本支出。

那怎么才能讲清楚研发效能的价值呢?我觉得可以通过间接收入、节约成本、开发者体验和业务质量提升四个方面来讲:

  1. 研发效能带来的收入

    1. 研发效能团队人均支持公司员工的数量、趋势

    2. 研发
      效能团队支持产研运团队的数量、趋势

    3. 研发效能团队支持产研运团队外的业务团队数量、趋势

  2. 研发效能节约的成本

    1. 员工、团队做与之
      前同样的事情,效率提高的数据

    2. 采用新技术节省了资源的投入,
      或同等资源支持了更多的业务发展

  3. 研发效能提高了开发者体验

    1. 效能平台给用户带来的开发者体验,比如业务对接的效率

    2. 效能平台用户 nps 评价

    3. 效能平台运营客服的响应速度和支持质量

    4. 业务方对研发效能团队、平台的用户访谈评价

  4. 研发效能带来的业务效率和质量整体提升

    1. 业务的整体端到端交付效率,比如需求交付周期、吞吐量

    2. 业务的整体质量提高,比如代码扫描高优问题解决趋势,上线成功率,回滚率

    3. 持续交付能力,比如代码提交到部署完成的时间,服务构建速度、频率和修复时长

上面只是给出一些可参考的方面。在公司具体落地实施时,还是要实事求是地以业务为纲,服务好公司业务部门,以做产品的高标准要求自己,服务好产研运团队,同时找到合适的数据来反
应我们的工作价值。

本文小结

用一两句话给+1/+2领导讲清楚研发效能的价值是非常不容易的,尤其是团队建设初期,没数据,没抓手,没背书,可见的只是人力物力的投入。领导也是知道研发效能是必须要做的,只不过什么时候做、做到什么程度、实现路径不是很确定,尤其是当还可以通过加资源(人力和物力)保持业务增长的时候。此时我就需要通过一些可见的数据、指标和图表,多方面地展现出公司研发效能整体的状况、可改进点和将来的效果,让他对研发效能的

务更有体感和理解,让他明白研发效能工作的价值和团队的价值。

我的其他文章

DevOps
|研发效能不是老板工程,是开发者服务

研发效能之技术治理

研发

能之产品运营

什么是

发效能?

研发效能定义及核心价值

二三线互联网公司怎么做好研发效能

感谢点赞、转载;关注我,了解研发效能发展动向;欢迎「DevOps研发效能」一起探讨;

scmroad 主要关注领域 { 研发效能、研发工具链、持续交付、DevOps、效能度量、微服务治理、容器、云原生},感谢订阅。

我们是
袋鼠云数栈 UED 团队
,致力于打造优秀的一站式数据中台产品。我们始终保持工匠精神,探索前端道路,为社区积累并传播经验价值。

本文作者:景明

我们以一段 C 代码为例,来看一下代码被编译成二进制可执行程序之后,是如何被 CPU 执行的。

在这段代码中,只是做了非常简单的加法操作,将 x 和 y 两个数字相加得到 z,并返回结果 z。

int main() {
    int x = 1;
    int y = 2;
    int z = x + y;
    return z;
}

我们知道,CPU 并不能直接执行这段 C 代码,而是需要对其进行编译,将其转换为二进制的机器码,然后 CPU 才能按照顺序执行编译后的机器码。

先通过 GCC 编译器将这段 C 代码编译成二进制文件,输入以下命令让其编译成目的文件:

gcc -O0 -o code_prog code.c

输入上面的命令之后回车,在文件夹中生成名为 code_prog 的可执行程序,接下来再将编译出来的 code_prog 程序进行反汇编,这样就可以看到二进制代码和对应的汇编代码。可以使用 objdump 的完成该任务,命令如下所示:

objdump -d code_prog

最后编译出来的机器码如下:

0000000100003f84 <_main>:
100003f84: ff 43 00 d1  	  sub	sp, sp, #16            // 开辟栈空间。即开辟了四个 4 字节空间
100003f88: ff 0f 00 b9  	  str	wzr, [sp, #12]         // 将 wzr 寄存器的数据存储到 sp 寄存器的 #12 地址上,设为0
100003f8c: 28 00 80 52  	  mov	w8, #1                 // 创建一个 x = 1,并将 1 存入 w8 寄存器中
100003f90: e8 0b 00 b9  	  str	w8, [sp, #8]           // 将 w8 寄存器的数据存入 sp 寄存器中 #8 的地址中,也就是将 x = 1 存入
100003f94: 48 00 80 52  	  mov	w8, #2                 // 创建一个 y = 2,并将 2 存入 w8 寄存器中
100003f98: e8 07 00 b9  	  str	w8, [sp, #4]           // 将 w8 寄存器的数据存入 sp 寄存器中 #4 的地址中,也就是将 y = 2 存入
100003f9c: e8 0b 40 b9  	  ldr	w8, [sp, #8]           // 读取 sp 寄存器中 #8 的数据存入 w8 寄存器中,也就是获取 x = 1
100003fa0: e9 07 40 b9  	  ldr	w9, [sp, #4]           // 读取 sp 寄存器中 #4 的数据存入 w9 寄存器中,也就是获取 y = 2
100003fa4: 08 01 09 0b  	  add	w8, w8, w9             // 将 w8、w9 寄存器的 x,y 数据进行相加,并存入 w8 寄存器中,也就是 z = 3
100003fa8: e8 03 00 b9  	  str	w8, [sp]               // 将 w8 寄存器的数据存入 sp 寄存器中
100003fac: e0 03 40 b9  	  ldr	w0, [sp]               // 读取 sp 寄存器中的数据存到 w0 寄存器中。z = 3
100003fb0: ff 43 00 91  	  add	sp, sp, #16            // 清空开辟的栈空间
100003fb4: c0 03 5f d6  	  ret                        // 返回结果

PS: wzr 为 32 的零寄存器,专门用来清零,也就是 sp 上 #12 指向的数据设置为 0

观察上方,左边就是编译生成的机器码,在这里它是使用十六进制来展示的,这主要是因为十六进制比较容易阅读,所以通常使用十六进制来展示二进制代码。

可以观察到上图是由很多行组成的,每一行都是一个指令,该指令可以让 CPU 执行指定的任务。

中间的部分是汇编代码,例如原本是二进制表示的指令,在汇编代码中可以使用单词来表示,比如 mov、add 就分别表示数据的存储和相加。

通常将汇编语言编写的程序转换为机器语言的过程称为“汇编”;反之,机器语言转化为汇编语言的过程称为“反汇编”,比如上图就是对 code_prog 进程进行了反汇编操作。

右边添加的注释,表示每条指令的具体含义。

这一大堆指令按照顺序集合在一起就组成了程序,所以程序的执行,本质上就是 CPU 按照顺序执行这一大堆指令的过程。

CPU 是怎么执行程序的?

为了更好的分析程序的执行过程,我们还需要了解一下基础的计算机硬件信息,具体如下图:

file

这张图是比较通用的系统硬件组织模型图,它主要是由 CPU、主存储器、各种 IO 总线,还有一些外部设备组成的。

首先,在一个程序执行之前,程序需要被装进内存,比如在 macOS 下面,你可以通过鼠标点击一个可执行文件,当你点击该文件的时候,系统中的程序加载器会将该文件加载到内存中。

CPU 可以通过指定内存地址,从内存中读取数据,或者往内存中写入数据,有了内存地址,CPU 和内存就可以有序地交互。

内存中的每个存储空间都有其对应的独一无二的地址:

file

在内存中,每个存放字节的空间都有其唯一的地址,而且地址是按照顺序排放的。

以开头代码为例,这段代码会被编译成可执行文件,可执行文件中包含了二进制的机器码,当二进制代码被加载进了内存后,那么内存中的每条二进制代码便都有了自己对应的地址,如下图所示:

file

一旦二进制代码被装载进内存,CPU 便可以从内存中取出一条指令,然后分析该指令,最后执行该指令。

把取出指令、分析指令、执行指令这三个过程称为一个 CPU 时钟周期。CPU 是永不停歇的,当它执行完成一条指令之后,会立即从内存中取出下一条指令,接着分析该指令,执行该指令,CPU 一直重复执行该过程,直至所有的指令执行完成。

CPU 是怎么知道要取出内存中的哪条指令呢?:

file

从上图可以看到 CPU 中有一个 PC 寄存器,它保存了将要执行的指令地址,当二进制代码被装载进了内存之后,系统会将二进制代码中的第一条指令的地址写入到 PC 寄存器中,到了下一个时钟周期时,CPU 便会根据 PC 寄存器中的地址,从内存中取出指令。

PC 寄存器中的指令取出来之后,系统要做两件事:第一件是将下一条指令的地址更新到 PC 寄存器中,如下图所示:

file

更新了 PC 寄存器之后,CPU 就会立即做第二件事,那就是分析该指令,并识别出不同的类型的指令,以及各种获取操作数的方法。

在指令分析完成之后,就要执行指令了。

在执行指令前,我们还需要认识一下 CPU 中的重要部件:寄存器。

寄存器

寄存器是 CPU 中用来存放数据的设备,不同处理器中寄存器的个数也是不一样的,之所要寄存器,是因为 CPU 访问内存的速度很慢,所以 CPU 就在内部添加了一些存储设备,这些设备就是寄存器。

他们的读取速度如下:

file

总结来说,寄存器容量小,读写速度快,内存容量大,读写速度慢。

寄存器通常用来存放数据或者内存中某块数据的地址,我们把这个地址又称为指针,通常情况下寄存器对存放的数据是没有特别的限制的,比如某个通用寄存器既可以存储数据,也可以存储指针。

不过由于历史原因,我们还会将某些专用的数据或者指针存储在专用的通用寄存器中 ,比如 rbp 寄存器通常用来存放栈帧指针的,rsp 寄存器用来存放栈顶指针的,PC 寄存器用来存放下一条要执行的指令等。

特殊寄存器

Stack Pointer register(SP)

The use of SP as an operand in an instruction, indicates the use of the current stack pointer.
指向当前栈指针。堆栈指针总是指向栈顶位置。一般堆栈的栈底不能动,所以数据入栈前要先修改堆栈指针,使它指向新的空余空间然后再把数据存进去,出栈的时候相反。

堆栈指针,随时跟踪栈顶地址,按"先进后出"的原则存取数据。

连接寄存器,一是用来保存子程序返回地址;二是当异常发生时,LR中保存的值等于异常发生时PC的值减4(或者减2),因此在各种异常模式下可以根据LR的值返回到异常发生前的相应位置继续执行。

Program Counter(PC)

A 64-bit Program Counter holding the address of the current instruction.
保存了将要执行的指令地址

Word Zero Register(WZR)

零寄存器,用于给int清零

tips

不同指令中寄存器后 #d 有什么区别?
[#d]在ARM代表的是一个常数表达式。
如:#0x3FC、#0、#0xF0000000、#200、#0xF0000001
都是代表着一个常数。

在 sp 寄存器中,代表的是当前栈顶指针移动的位置。
如:

sub	sp, sp, #16;// 获取 sp 中的栈顶指针移动 16位的位置,并把位置更新到 sp 寄存器中。实现开辟空间

在通用寄存器 W0 - W11 中,代表的操作的常数值。

mov	w8, #2,// 把常数 2 添加到 w8 寄存器中

通用寄存器

以下介绍下比较常见的通用寄存器:

  • 其中W0~W3 用于函数调用入参,其中,W0 还用于程序的返回值.
  • W4~W11用于保存局部变量。
  • W13为SP,时刻指向栈顶,当有数据入栈或出栈时,需要更新SP
  • W14为链接寄存器,主要是用作保存子程序返回的地址。
  • W15为PC寄存器,指向将要执行的下一条指令地址。

常见指令

mov

数据传送指令。将立即数或寄存器(operant2)传送到目标寄存器Rd,可用于移位运算等操作。指令格式如下:

MOV{cond}{S} Rd,operand2

如:

mov w8, #1
,就是往 w8 寄存器中写入 #1.

mov w8, w9
, 就是把 w9 寄存器的数据发送到 w8 寄存器中,最终 w8 和 w9 寄存器的数据一致。如下图:

file

ldr

ldr 从内存中读取数据放入寄存器中

LDR{cond}{T} Rd,<地址>;加载指定地址上的数据(字),放入Rd中

如:

ldr w8, [sp, #8]
读取 sp 寄存器中 #8 位置的数据存入 w8 寄存器中,改变的只有 w8 ,sp 寄存器不变

str

str 指令用于将寄存器中的数据保存到内存

STR{cond}{T} Rd,<地址>;存储数据(字)到指定地址的存储单元,要存储的数据在Rd中

如:
str w8, [sp]
, 将 w8 寄存器的数据存入 sp 寄存器中

add

加法运算指令。将operand2 数据与Rn 的值相加,结果保存到Rd 寄存器。指令格式如下:

ADD{cond}{S} Rd,Rn,operand2


add w8, w8, w9
为例,就是把 w8、w9 寄存器的 x,y 数据进行相加,并存入 w8 寄存器中

如下图:

file

sub

减法运算指令。用寄存器 Rn 减去operand2。结果保存到 Rd 中。指令格式如下:

SUB{cond}{S} Rd,Rn,operand2

如:

sub R0,R0,#1
-- R0=R0-1

执行过程

了解了以上的知识,我们再来分析一遍代码的执行过程。

在 C 程序中,CPU 会首先执行调用 main 函数,在调用 main 函数时,生成一块内存空间,用来存放 main 函数执行过程中的数据。

sub	sp, sp, #16

将 0 写入到 #12 的字节位置上。

str	wzr, [sp, #12]

接下来给 x 附值

mov	w8, #1
str	w8, [sp, #8]

第一行指令是把 1 添加进寄存器中。第二行指令是把 1 存入 #8 地址的内存空间中。

接着给 y 附值

mov	w8, #2
str	w8, [sp, #4]

第一行指令是把 2 添加进寄存器中。第二行指令是把 2 存入 #4 地址的内存空间中。

执行完 x, y 的生成,接下来执行
z = x + y

ldr	w8, [sp, #8]
ldr	w9, [sp, #4]
add	w8, w8, w9

第一行指令取出内存空间地址为 #8 的数据,也就是 1. 第二行指令去除内存空间地址为 #4 的数据,也就是 2,第三行指令则对取出的数据进行相加操作,并将结果 3 存入寄存器中。

str	w8, [sp]
ldr	w0, [sp]

第一行指令把寄存器中的最终的数据存入内存中,第二行指令则获取内存中的结果,存入寄存器中。等待返回

add	sp, sp, #16

把开辟的空间进行清理。

ret

返回结果

总结


本文主要讲解了 CPU 的执行过程,顺便了解了一下基础的计算机硬件信息,如有想法

一句话来解释什么是深浅拷贝,
B拷贝A,当修改A,B如果变化,就是浅拷贝,反之就是深拷贝

基本原理:

1.递归函数
2.对象内的值都是简单数据类型时 直接进行赋值
3.当我们遇到数组和对象时,可以再次调用函数,利用递归去拷贝数组和对象内的每个值
4.先数组 后对象  因为数组也是对象

下面是一个实现深拷贝的函数:

1 functiondeepClone(obj) {2         let objClone = Array.isArray(obj) ?[] : {};3         if (obj && typeof obj === "object") {4             for (key inobj) {5                 if(obj.hasOwnProperty(key)) {6                     //判断ojb子元素是否为对象(复杂数据类型),如果是,递归复制
7                     if (obj[key] && typeof obj[key] === "object") {8                         objClone[key] =deepClone(obj[key]);9                     } else{10                         //如果不是,简单复制(基本数据类型)
11                         objClone[key] =obj[key];12 }13 }14 }15 }16         returnobjClone;17     }