2024年11月

将Apache Samza作业迁移到Apache Flink作业是一个复杂的任务,因为这两个流处理框架有不同的API和架构。然而,我们可以将Samza作业的核心逻辑迁移到Flink,并尽量保持功能一致。

假设我们有一个简单的Samza作业,它从Kafka读取数据,进行一些处理,然后将结果写回到Kafka。我们将这个逻辑迁移到Flink。

1. Samza 作业示例

首先,让我们假设有一个简单的Samza作业:

// SamzaConfig.java
import org.apache.samza.config.Config;
import org.apache.samza.config.MapConfig;
import org.apache.samza.serializers.JsonSerdeFactory;
import org.apache.samza.system.kafka.KafkaSystemFactory;
 
import java.util.HashMap;
import java.util.Map;
 
public class SamzaConfig {
    public static Config getConfig() {
        Map<String, String> configMap = new HashMap<>();
        configMap.put("job.name", "samza-flink-migration-example");
        configMap.put("job.factory.class", "org.apache.samza.job.yarn.YarnJobFactory");
        configMap.put("yarn.package.path", "/path/to/samza-job.tar.gz");
        configMap.put("task.inputs", "kafka.my-input-topic");
        configMap.put("task.output", "kafka.my-output-topic");
        configMap.put("serializers.registry.string.class", "org.apache.samza.serializers.StringSerdeFactory");
        configMap.put("serializers.registry.json.class", JsonSerdeFactory.class.getName());
        configMap.put("systems.kafka.samza.factory", KafkaSystemFactory.class.getName());
        configMap.put("systems.kafka.broker.list", "localhost:9092");
 
        return new MapConfig(configMap);
    }
}
 
// MySamzaTask.java
import org.apache.samza.application.StreamApplication;
import org.apache.samza.application.descriptors.StreamApplicationDescriptor;
import org.apache.samza.config.Config;
import org.apache.samza.system.IncomingMessageEnvelope;
import org.apache.samza.system.OutgoingMessageEnvelope;
import org.apache.samza.system.SystemStream;
import org.apache.samza.task.MessageCollector;
import org.apache.samza.task.TaskCoordinator;
import org.apache.samza.task.TaskContext;
import org.apache.samza.task.TaskInit;
import org.apache.samza.task.TaskRun;
import org.apache.samza.serializers.JsonSerde;
 
import java.util.HashMap;
import java.util.Map;
 
public class MySamzaTask implements StreamApplication, TaskInit, TaskRun {
    private JsonSerde<String> jsonSerde = new JsonSerde<>();
 
    @Override
    public void init(Config config, TaskContext context, TaskCoordinator coordinator) throws Exception {
        // Initialization logic if needed
    }
 
    @Override
    public void run() throws Exception {
        MessageCollector collector = getContext().getMessageCollector();
        SystemStream inputStream = getContext().getJobContext().getInputSystemStream("kafka", "my-input-topic");
 
        for (IncomingMessageEnvelope envelope : getContext().getPoll(inputStream, "MySamzaTask")) {
            String input = new String(envelope.getMessage());
            String output = processMessage(input);
            collector.send(new OutgoingMessageEnvelope(getContext().getOutputSystem("kafka"), "my-output-topic", jsonSerde.toBytes(output)));
        }
    }
 
    private String processMessage(String message) {
        // Simple processing logic: convert to uppercase
        return message.toUpperCase();
    }
 
    @Override
    public StreamApplicationDescriptor getDescriptor() {
        return new StreamApplicationDescriptor("MySamzaTask")
                .withConfig(SamzaConfig.getConfig())
                .withTaskClass(this.getClass());
    }
}

现在,让我们将这个Samza作业迁移到Flink:

// FlinkConfig.java
import org.apache.flink.configuration.Configuration;
 
public class FlinkConfig {
    public static Configuration getConfig() {
        Configuration config = new Configuration();
        config.setString("execution.target", "streaming");
        config.setString("jobmanager.rpc.address", "localhost");
        config.setInteger("taskmanager.numberOfTaskSlots", 1);
        config.setString("pipeline.execution.mode", "STREAMING");
        return config;
    }
}
 
// MyFlinkJob.java
import org.apache.flink.api.common.functions.MapFunction;
import org.apache.flink.api.common.serialization.SimpleStringSchema;
import org.apache.flink.streaming.api.datastream.DataStream;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.connectors.kafka.FlinkKafkaConsumer;
import org.apache.flink.streaming.connectors.kafka.FlinkKafkaProducer;
 
import java.util.Properties;
 
public class MyFlinkJob {
    public static void main(String[] args) throws Exception {
        // Set up the execution environment
        final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
 
        // Configure Kafka consumer
        Properties properties = new Properties();
        properties.setProperty("bootstrap.servers", "localhost:9092");
        properties.setProperty("group.id", "flink-consumer-group");
 
        FlinkKafkaConsumer<String> consumer = new FlinkKafkaConsumer<>("my-input-topic", new SimpleStringSchema(), properties);
 
        // Add source
        DataStream<String> stream = env.addSource(consumer);
 
        // Process the stream
        DataStream<String> processedStream = stream.map(new MapFunction<String, String>() {
            @Override
            public String map(String value) throws Exception {
                return value.toUpperCase();
            }
        });
 
        // Configure Kafka producer
        FlinkKafkaProducer<String> producer = new FlinkKafkaProducer<>("my-output-topic", new SimpleStringSchema(), properties);
 
        // Add sink
        processedStream.addSink(producer);
 
        // Execute the Flink job
        env.execute("Flink Migration Example");
    }
}

3. 运行Flink作业

(1)
设置Flink环境
:确保你已经安装了Apache Flink,并且Kafka集群正在运行。

(2)编译和运行:

  • 使用Maven或Gradle编译Java代码。
  • 提交Flink作业到Flink集群或本地运行。
# 编译(假设使用Maven)
mvn clean package
 
# 提交到Flink集群(假设Flink在本地运行)
./bin/flink run -c com.example.MyFlinkJob target/your-jar-file.jar

4. 注意事项

  • 依赖管理
    :确保在
    pom.xml

    build.gradle
    中添加了Flink和Kafka的依赖。
  • 序列化
    :Flink使用
    SimpleStringSchema
    进行简单的字符串序列化,如果需要更复杂的序列化,可以使用自定义的序列化器。
  • 错误处理
    :Samza和Flink在错误处理方面有所不同,确保在Flink中适当地处理可能的异常。
  • 性能调优
    :根据实际需求对Flink作业进行性能调优,包括并行度、状态后端等配置。

这个示例展示了如何将一个简单的Samza作业迁移到Flink。

【1】引言(完整代码在最后面)

在鸿蒙NEXT系统中,开发一个有趣且实用的转盘应用不仅可以提升用户体验,还能展示鸿蒙系统的强大功能。本文将详细介绍如何使用鸿蒙NEXT系统开发一个转盘应用,涵盖从组件定义到用户交互的完整过程。

【2】环境准备

电脑系统:windows 10

开发工具:DevEco Studio NEXT Beta1 Build Version: 5.0.3.806

工程版本:API 12

真机:mate60 pro

语言:ArkTS、ArkUI

【3】难点分析

1. 扇形路径的计算

难点:创建扇形的路径需要精确计算起始点、结束点和弧线参数。尤其是涉及到三角函数的使用,初学者可能会对如何将角度转换为坐标感到困惑。

解决方案:可以通过绘制简单的示意图来帮助理解扇形的构造,并在代码中添加详细注释,解释每一步的计算过程。

2. 动态角度计算

难点:在转盘旋转时,需要根据单元格的比例动态计算每个单元格的角度和旋转角度。这涉及到累加和比例计算,可能会导致逻辑错误。

解决方案:使用数组的 reduce 方法来计算总比例,并在计算每个单元格的角度时,确保逻辑清晰。可以通过单元测试来验证每个单元格的角度是否正确。

3. 动画效果的实现

难点:实现转盘的旋转动画需要对动画的持续时间、曲线和结束后的状态进行管理。初学者可能会对如何控制动画的流畅性和效果感到困惑。

解决方案:可以参考鸿蒙NEXT的动画文档,了解不同的动画效果和参数设置。通过逐步调试,观察动画效果并进行调整。

4. 用户交互的处理

难点:处理用户点击事件,尤其是在动画进行时,如何禁用按钮以防止重复点击,可能会导致状态管理的复杂性。

解决方案:在按钮的点击事件中,使用状态变量(如 isAnimating)来控制按钮的可用性,并在动画结束后恢复按钮的状态。

5. 组件的状态管理

难点:在多个组件之间传递状态(如当前选中的单元格、转盘的角度等)可能会导致状态管理混乱。

解决方案:使用状态管理工具(如 @State 和 @Trace)来确保状态的统一管理,并在需要的地方进行状态更新,保持组件之间的解耦。

【完整代码】

import { CounterComponent, CounterType } from '@kit.ArkUI'; // 导入计数器组件和计数器类型

// 定义扇形组件
@Component
struct Sector {
  @Prop radius: number; // 扇形的半径
  @Prop angle: number; // 扇形的角度
  @Prop color: string; // 扇形的颜色

  // 创建扇形路径的函数
  createSectorPath(radius: number, angle: number): string {
    const centerX = radius / 2; // 计算扇形中心的X坐标
    const centerY = radius / 2; // 计算扇形中心的Y坐标
    const startX = centerX; // 扇形起始点的X坐标
    const startY = centerY - radius; // 扇形起始点的Y坐标
    const halfAngle = angle / 4; // 计算半个角度

    // 计算扇形结束点1的坐标
    const endX1 = centerX + radius * Math.cos((halfAngle * Math.PI) / 180);
    const endY1 = centerY - radius * Math.sin((halfAngle * Math.PI) / 180);

    // 计算扇形结束点2的坐标
    const endX2 = centerX + radius * Math.cos((-halfAngle * Math.PI) / 180);
    const endY2 = centerY - radius * Math.sin((-halfAngle * Math.PI) / 180);

    // 判断是否为大弧
    const largeArcFlag = angle / 2 > 180 ? 1 : 0;
    const sweepFlag = 1; // 设置弧线方向为顺时针

    // 生成SVG路径命令
    const pathCommands =
      `M${startX} ${startY} A${radius} ${radius} 0 ${largeArcFlag} ${sweepFlag} ${endX1} ${endY1} L${centerX} ${centerY} L${endX2} ${endY2} A${radius} ${radius} 0 ${largeArcFlag} ${1 -
        sweepFlag} ${startX} ${startY} Z`;
    return pathCommands; // 返回路径命令
  }

  // 构建扇形组件
  build() {
    Stack() {
      // 创建第一个扇形路径
      Path()
        .width(`${this.radius}px`) // 设置宽度为半径
        .height(`${this.radius}px`) // 设置高度为半径
        .commands(this.createSectorPath(this.radius, this.angle)) // 设置路径命令
        .fillOpacity(1) // 设置填充透明度
        .fill(this.color) // 设置填充颜色
        .strokeWidth(0) // 设置边框宽度为0
        .rotate({ angle: this.angle / 4 - 90 }); // 旋转扇形

      // 创建第二个扇形路径
      Path()
        .width(`${this.radius}px`) // 设置宽度为半径
        .height(`${this.radius}px`) // 设置高度为半径
        .commands(this.createSectorPath(this.radius, this.angle)) // 设置路径命令
        .fillOpacity(1) // 设置填充透明度
        .fill(this.color) // 设置填充颜色
        .strokeWidth(0) // 设置边框宽度为0
        .rotate({ angle: 180 - (this.angle / 4 - 90) }); // 旋转扇形
    }
  }
}

// 定义单元格类
@ObservedV2
class Cell {
  @Trace angle: number = 0; // 扇形的角度
  @Trace title: string; // 当前格子的标题
  @Trace color: string; // 背景颜色
  @Trace rotate: number = 0; // 在转盘要旋转的角度
  angleStart: number = 0; // 轮盘所在区间的起始
  angleEnd: number = 0; // 轮盘所在区间的结束
  proportion: number = 0; // 所占比例

  // 构造函数
  constructor(proportion: number, title: string, color: string) {
    this.proportion = proportion; // 设置比例
    this.title = title; // 设置标题
    this.color = color; // 设置颜色
  }
}

// 定义转盘组件
@Entry
@Component
struct Wheel {
  @State cells: Cell[] = []; // 存储单元格的数组
  @State wheelWidth: number = 600; // 转盘的宽度
  @State currentAngle: number = 0; // 当前转盘的角度
  @State selectedName: string = ""; // 选中的名称
  isAnimating: boolean = false; // 动画状态
  colorIndex: number = 0; // 颜色索引
  colorPalette: string[] = [ // 颜色调色板
    "#26c2ff",
    "#978efe",
    "#c389fe",
    "#ff85bd",
    "#ff7051",
    "#fea800",
    "#ffcf18",
    "#a9c92a"
  ];

  // 组件即将出现时调用
  aboutToAppear(): void {
    // 初始化单元格
    this.cells.push(new Cell(1, "跑步", this.colorPalette[this.colorIndex++ % this.colorPalette.length]));
    this.cells.push(new Cell(2, "跳绳", this.colorPalette[this.colorIndex++ % this.colorPalette.length]));
    this.cells.push(new Cell(1, "唱歌", this.colorPalette[this.colorIndex++ % this.colorPalette.length]));
    this.cells.push(new Cell(4, "跳舞", this.colorPalette[this.colorIndex++ % this.colorPalette.length]));

    this.calculateAngles(); // 计算角度
  }

  // 计算每个单元格的角度
  private calculateAngles() {
    // 根据比例计算总比例
    const totalProportion = this.cells.reduce((sum, cell) => sum + cell.proportion, 0);
    this.cells.forEach(cell => {
      cell.angle = (cell.proportion * 360) / totalProportion; // 计算每个单元格的角度
    });

    let cumulativeAngle = 0; // 累计角度
    this.cells.forEach(cell => {
      cell.angleStart = cumulativeAngle; // 设置起始角度
      cumulativeAngle += cell.angle; // 更新累计角度
      cell.angleEnd = cumulativeAngle; // 设置结束角度
      cell.rotate = cumulativeAngle - (cell.angle / 2); // 计算旋转角度
    });
  }

  // 构建转盘组件
  build() {
    Column() {
      Row() {
        Text('转盘').fontSize(20).fontColor("#0b0e15"); // 显示转盘标题
      }.width('100%').height(44).justifyContent(FlexAlign.Center); // 设置行的宽度和高度

      // 显示当前状态
      Text(this.isAnimating ? '旋转中' : `${this.selectedName}`).fontSize(20).fontColor("#0b0e15").height(40);

      Stack() {
        Stack() {
          // 遍历每个单元格并绘制扇形
          ForEach(this.cells, (cell: Cell) => {
            Stack() {
              Sector({ radius: lpx2px(this.wheelWidth) / 2, angle: cell.angle, color: cell.color }); // 创建扇形
              Text(cell.title).fontColor(Color.White).margin({ bottom: `${this.wheelWidth / 1.4}lpx` }); // 显示单元格标题
            }.width('100%').height('100%').rotate({ angle: cell.rotate }); // 设置宽度和高度,并旋转
          });
        }
        .borderRadius('50%') // 设置圆角
        .backgroundColor(Color.Gray) // 设置背景颜色
        .width(`${this.wheelWidth}lpx`) // 设置转盘宽度
        .height(`${this.wheelWidth}lpx`) // 设置转盘高度
        .rotate({ angle: this.currentAngle }); // 旋转转盘

        // 创建指针
        Polygon({ width: 20, height: 10 })
          .points([[0, 0], [10, -20], [20, 0]]) // 设置指针的点
          .fill("#d72b0b") // 设置指针颜色
          .height(20) // 设置指针高度
          .margin({ bottom: '140lpx' }); // 设置指针底部边距

        // 创建开始按钮
        Button('开始')
          .fontColor("#c53a2c") // 设置按钮字体颜色
          .borderWidth(10) // 设置按钮边框宽度
          .borderColor("#dd2218") // 设置按钮边框颜色
          .backgroundColor("#fde427") // 设置按钮背景颜色
          .width('200lpx') // 设置按钮宽度
          .height('200lpx') // 设置按钮高度
          .borderRadius('50%') // 设置按钮为圆形
          .clickEffect({ level: ClickEffectLevel.LIGHT }) // 设置点击效果
          .onClick(() => { // 点击按钮时的回调函数
            if (this.isAnimating) { // 如果正在动画中,返回
              return;
            }
            this.selectedName = ""; // 清空选中的名称
            this.isAnimating = true; // 设置动画状态为正在动画
            animateTo({ // 开始动画
              duration: 5000, // 动画持续时间为5000毫秒
              curve: Curve.EaseInOut, // 动画曲线为缓入缓出
              onFinish: () => { // 动画完成后的回调
                this.currentAngle %= 360; // 保持当前角度在0到360之间
                for (const cell of this.cells) { // 遍历每个单元格
                  // 检查当前角度是否在单元格的角度范围内
                  if (360 - this.currentAngle >= cell.angleStart && 360 - this.currentAngle <= cell.angleEnd) {
                    this.selectedName = cell.title; // 设置选中的名称为当前单元格的标题
                    break; // 找到后退出循环
                  }
                }
                this.isAnimating = false; // 设置动画状态为未动画
              },
            }, () => { // 动画进行中的回调
              this.currentAngle += (360 * 5 + Math.floor(Math.random() * 360)); // 更新当前角度,增加随机旋转
            });
          });
      }

      // 创建滚动区域
      Scroll() {
        Column() {
          // 遍历每个单元格,创建输入框和计数器
          ForEach(this.cells, (item: Cell, index: number) => {
            Row() {
              // 创建文本输入框,显示单元格标题
              TextInput({ text: item.title })
                .layoutWeight(1) // 设置输入框占据剩余空间
                .onChange((value) => { // 输入框内容变化时的回调
                  item.title = value; // 更新单元格标题
                });
              // 创建计数器组件
              CounterComponent({
                options: {
                  type: CounterType.COMPACT, // 设置计数器类型为紧凑型
                  numberOptions: {
                    label: `当前占比`, // 设置计数器标签
                    value: item.proportion, // 设置计数器初始值
                    min: 1, // 设置最小值
                    max: 100, // 设置最大值
                    step: 1, // 设置步长
                    onChange: (value: number) => { // 计数器值变化时的回调
                      item.proportion = value; // 更新单元格的比例
                      this.calculateAngles(); // 重新计算角度
                    }
                  }
                }
              });
              // 创建删除按钮
              Button('删除').onClick(() => {
                this.cells.splice(index, 1); // 从单元格数组中删除当前单元格
                this.calculateAngles(); // 重新计算角度
              });
            }.width('100%').justifyContent(FlexAlign.SpaceBetween) // 设置行的宽度和内容对齐方式
            .padding({ left: 40, right: 40 }); // 设置左右内边距
          });
        }.layoutWeight(1); // 设置滚动区域占据剩余空间
      }.layoutWeight(1) // 设置滚动区域占据剩余空间
      .margin({ top: 20, bottom: 20 }) // 设置上下外边距
      .align(Alignment.Top); // 设置对齐方式为顶部对齐

      // 创建添加新内容按钮
      Button('添加新内容').onClick(() => {
        // 向单元格数组中添加新单元格
        this.cells.push(new Cell(1, "新内容", this.colorPalette[this.colorIndex++ % this.colorPalette.length]));
        this.calculateAngles(); // 重新计算角度
      }).margin({ top: 20, bottom: 20 }); // 设置按钮的上下外边距
    }
    .height('100%') // 设置组件高度为100%
    .width('100%') // 设置组件宽度为100%
    .backgroundColor("#f5f8ff"); // 设置组件背景颜色
  }
}

1. 存储器层次(The Memory Hierarchy)

1.1 现代系统中的存储器

image-20241107185003120

其中包括L1、L2、L3和DRAM

1.2 存储器的局限

理想存储器的需求如下:

  • 零延迟
  • 容量无限
  • 零成本
  • 带宽无限
  • 零功耗

但理想存储器的需求彼此冲突:

  • 容量更大的存储器意味着更大的延迟:需要花更长的时间来确定数据所在位置
  • 更快的存储器价格更贵
    • 存储器工艺:SRAM vs. DRAM vs. Disk vs. Tape
  • 更高带宽的存储器更贵
    • 需要更多的banks,更多的端口,更快的工作频率,更快的工艺

1.3 为什么使用存储器分层结构

我们想要存储器又快,容量又大,这并不能通过单层存储器实现。

Idea:
多层次存储(离处理器越远的存储器容量越大,速度越慢)并且确保处理器所需的大部分数据能够保持在较快的层次中。

Memory System:

image-20241107190608552
image-20241107190838875

只要拥有良好的
引用局部性
,存储器层次结构可以使得存储器又快,容量又大。

1.4 存储器分层结构

image-20241107191126551

从小到大,从快到慢依次包括:
寄存器(Register File)

缓存(Cache)

主存储器(Main Memory)

硬盘(Hard Disk)

1.5 Locality(局部性)

  • 通过一个人不久的过去能够预测其不远的将来

  • 时间局部性(Temporal Locality):
    如果你刚好干了某事,那么你极有可能很快再干同样的事情。


    • 你今天在这间教室,那么将有很大可能你将会有规律地一次又一次出现在这个教室。
  • 空间局部性(Spatial Locality):
    如果你做了某件事情,你很可能会做类似/相关的事情(在空间上)。


    • 每次我在这间教室找到你时,你可能都和同样的人坐得很近,或者坐在很近的座位上。

1.6 内存局部性

  • 一个”典型“程序在内存引用方面有很多局部性

    • loop
      组成的程序
  • 时间:
    程序往往会在一小段时间内多次引用同一内存位置
  • 空间:
    程序倾向于在一个时间窗口内引用附近的内存位置
    • 指令内存引用 → 通常是顺序/流式(顺序执行指令)
    • 对数组/向量的引用 → 通常是流式/串式的

1.7 缓存基础

1.7.1 利用时间局部性

  • Idea:
    将最近访问过的数据存储在自动管理的快速内存(称为缓存(cache))
  • 预期结果:
    相同的存储器地址将很快被访问

1.7.2 利用空间局部性

  • Idea:
    在自动管理的快速存储器中,将数据存储在与最近访问的地址相邻的地址中
    • 逻辑上将内存划分为大小相等的区块
    • 将访问的区块整个提取到缓存中
  • 预期结果:
    与当前访问内存位置相邻的内存位置将很快被访问

2. 缓存分层结构

2.1 流水线设计中的缓存

  • 缓存需要与流水线紧密集成


    • 理想情况下希望能在1个周期内访问数据,这样依赖load的操作才不会停滞
  • 然而高频流水线 → 不能让 cache 太大


    • 但是,我们需要大的 cache 和流水线设计
  • Idea:缓存层次结构

image-20241107200809335

其中,一级缓存(L1)设计决策主要由处理器的频率决定,并且L1非常小;二级缓存(L2)比L1大,但访问速度也比L1慢。

2.2 最基本的Cache结构

image-20241107203143500

其中包括一个标签存储(Tag Store)和一个数据存储(Data Store)。

当处理器需要查询缓存时,它需要向Tag Store发送一个地址。

如果Tag Store显示该地址存在缓存中,那么你可以从Data Store中获取数据。

如果Tag Store显示该地址不在缓存中,那么你必须去主内存取数据。

2.3 存储器分层结构设计分析

image-20241107204140331

2.3.1 分层延迟分析

  • 程序查询该层,会得到访问数据命中或未命中(Hit/Miss),这意味着数据在该层或不在。

  • 假设给定的存储器分层结构第
    \(i\)
    层具有
    固有访问时间(technology-intrinsic access time)
    \(t_i\)

  • 但实际的
    感知访问时间(perceived access time)
    \(T_i\)

    要比
    \(t_i\)
    长。

  • 除了最外层的层次结构外,在查找给定地址时,有以下几种情况


    • 当你命中”hit“的时候(命中率 hit-rate
      \(h_i\)
      ),访问时间为
      \(t_i\)
    • 当你未命中”miss“的时候(未命中率 miss-rate
      \(m_i\)
      ),访问时间为
      \(t_i + T_{i+1}\)
    • \(h_i + m_i = 1\)
  • 因此
    \(T_i = h_i \cdot t_i + m_i \cdot (t_i + T_{i+1})\)
    ,即
    \(T_i = t_i + m_i \cdot T_{i+1}\)

2.3.2 分层结构设计考虑因素

  • 递归延迟方程


    \[T_i = t_i + m_i \cdot T_{i+1}
    \]

  • 目标:
    在允许的成本范围内实现所需的
    \(T_1\)

  • 希望
    \(T_i \approx t_i\)
    ,虽然这是不可能的

  • 降低未命中率
    \(m_i\)


    • 增加容量
      \(C_i\)
      可降低
      \(m_i\)
      ,但这样会增加固有访问时间
      \(t_i\)
    • 通过更智能的缓存管理(替换预测不需要的内容,预取预测需要的内容)降低
      \(m_i\)
  • 降低下一层级的感知访问时间
    \(T_{i+1}\)


    • 使用更快的层级,但要注意成本的增加
    • 引入中间层次作为妥协

2.3.3 举例:Intel奔腾 4

  • 90nm P4, 3.6 GHz
  • L1 D-cache
    • \(C_1\)
      = 16 kB
    • \(t_1\)
      = 4 cyc int / 9 cycle fp
  • L2 D-cache
    • \(C_2\)
      = 1024 kB
    • \(t_2\)
      = 18 cyc int / 18 cyc fp
  • Main memory
    • \(t_3\)
      = ~ 50ns or 180 cyc

\(T_i = t_i + m_i\cdot T_{i+1}\)

  • if
    \(m_1=0.1, m_2=0.1\)
    • \(T_2=t_2 + m_2T_3 = 18+0.1\times 180 =36cyc\)
    • \(T_1=t_1+m_1T_2=4+0.1\times36=7.6cyc\)
  • if
    \(m_1=0.01, m_2=0.01\)
    • \(T_1=4.2, T_2=19.8\)
  • if
    \(m_1=0.05, m_2=0.01\)
    • \(T_1=5.00, T_2=19.8\)
  • if
    \(m_1=0.01, m_2=0.50\)
    • \(T_1=5.08, T_2=108\)

3. 缓存基础和操作

3.1 缓存

  • 任何 "存储" 已使用(或已生成)数据的结构
    • 以避免重复从头开始复制/获取数据所需的长延时操作
    • e.g. a web cache
  • 在处理器设计中最常见的是:自动管理的内存结构
    • e.g. 在快速 SRAM 中存储最频繁或最近访问过的 DRAM 内存位置,以避免重复支付 DRAM 访问延迟费用
    • 管理策略:
      • What data bring to cache?
      • What data keep in cache?

3.2 基本硬件缓存

  • 我们将从基本的硬件缓存示例和缓存操作开始
  • 然后,我们将正式确定一些缓存的基本概念
  • 最后,我们将研究各种想法和创新,以提高缓存性能

3.2.1 访问缓存

image-20241107225134473

  • 缓存大小:
    缓存的总大小是64字节,由8个8字节的存储单元(8-byte words)组成,每个存储单元可以存储8个字节的数据。

  • 字节地址(Byte Address):
    后3位表示字节的
    Offset
    ,由于每个存储单元为8-byte。

  • 索引位(Index Bits):
    有3位
    index bits
    ,由于有8个缓存行(
    \(2^3 = 8\)
    ),每行可以存储一个8字节的数据块。

直接映射缓存(Direct-mapped Cache)
:每个主存地址直接映射到缓存中的唯一位置。地址中的索引位决定数据在缓存中的位置,所以每个地址都会对应到一个唯一的缓存行。

3.2.2 Tag Array(标签数组)

image-20241107230424748

当主存地址映射到某一缓存行时,系统会将该地址的Tag值与标记数组中的Tag值进行比较,以判断数据是否在缓存中。
如果Tag值匹配,说明数据已在缓存中(命中);如果不匹配,则说明需要从主存中加载该数据(未命中)。

3.2.3 增加 Block 大小

image-20241107231201653

从每行存储8-byte数据变为每行存储32-byte数据:

  • 缓存大小:
    缓存的总大小是256字节,由8个32字节的存储单元(32-byte words)组成,每个存储单元可以存储32个字节的数据。

  • 字节地址(Byte Address):
    后5位表示字节的
    Offset
    ,由于每个存储单元为32-byte。

  • 索引位(Index Bits):
    有3位
    index bits
    ,由于有8个缓存行(
    \(2^3 = 8\)
    ),每行可以存储一个32字节的数据块。

更大的缓存行的影响

  • 较小的标签数组(Tag Array)
    :因为每个缓存行可以容纳更多数据,因此只需较少的行数来覆盖同样的数据范围,减少了Tag数组的大小。
  • 减少缓存缺失
    :由于
    空间局部性(Spatial Locality)
    ,访问某个数据块后,往往会访问附近的数据。较大的缓存行可以一次性加载更多相邻数据,减少后续访问时的缺失率。

3.2.4 关联性(Associativity)

image-20241107232437313

两路组相联缓存(2-way set associative cache)的结构:

  • 标记数组(Tag Array)
    :由于是两路组相联缓存,每个地址会对应多个”路“(Way)。在这个例子中有两条路(Way-1 和 Way-2),因此标记数组包含两组标签,以允许每组缓存行可以存储不同的数据块。
  • 数据数组(Data Array)
    :数据分为两条路(Way-1 和 Way-2),每个路都有自己的数据存储块。相比于直接映射缓存,组相联缓存能够容纳更多的相同索引的数据块,从而减少缓存冲突。
  • 比较(Compare)
    :当进行读取操作时,Tag数组中的两个标签(对应 Way-1 和 Way-2)都需要与请求的地址Tag进行比较,以确定是否有缓存命中。若其中一个路的标签与请求的地址匹配,则表示缓存命中,数据可以从对应的路中读取。

组相联的优势与劣势

  • 优势
    :减少了缓存冲突,因为多个数据块可以映射到同一个缓存位置,解决了直接映射缓存中的冲突问题。
  • 劣势
    :功耗增加,因为需要同时读取多个标签和数据块,以进行匹配和选择。

3.2.5 Example

32 KB 4 路组关联缓存阵列,行大小为 32 字节,假设地址为 40 位

  • How many sets?

    \(32KB / (32Byte\times4)=32KB/128Byte=256set\)

  • How many index bits, offset bits, tag bits?

    index bits(8):
    \(2^8=256set\)

    offset bits(5):
    \(2^{5}=32Byte\)

    tag bits(27):
    \(40-5-8=27bits\)

  • How large is the tag array?

\(27bit \times 256 \times 4=27Kb\)

3.3 缓存 Block/Line

  • Block(line)(缓存块):缓存中的存储单元
    • 内存在逻辑上被划分为缓存块,这些缓存块映射到缓存中的位置
    • 一个固定大小的数据集合,
      包含所请求的字
      ,从主存储器中取出并放入缓存中

image-20241108104328007
image-20241108104437312

  • 对于引用:


    • HIT:
      如果在缓存中,则使用缓存数据,而不是访问内存
    • MISS:
      如果不在缓存中,则将数据块引入缓存
      • 可能需要将其他东西踢出缓存才能做到这一点 (替换
  • 一些重要的缓存设计决策


    • 放置:在缓存中何处以及如何放置/查找/索引块?
    • 替换:删除/执行/替换哪些数据以在缓存中腾出空间?
    • 管理粒度:大块还是小块?
    • 写入策略:如何处理写入?Write-back、Write-through
    • 指令/数据:我们是否将它们分开处理?

3.4 缓存 Miss 的类型

  • 强制未命中:
    首次访问内存 word 时发生——无限缓存的未命中次数
  • 容量未命中:
    发生这种情况是因为程序在重新接触同一 word 之前接触了许多其他单词——完全关联缓存的缺失量
  • 冲突未命中:
    由于两个 word 映射到高速缓存中的相同位置而发生——从完全关联高速缓存到直接映射高速缓存时产生的缺失

附注:
全关联缓存的未命中次数会比相同大小的直接映射缓存多吗?会,增加了冲突未命中

3.5 降低 Miss Rate

  • 增加缓存块大小
    :增大缓存块的大小可以减少强制性未命中(即数据首次被访问时的未命中),并且在存在空间局部性时,可以降低未命中的惩罚(因为可以一次性预取更多相关数据)。但增大块大小也带来一些负面影响,比如:


    • 不同缓存层之间的流量增加

    • 可能导致空间浪费(当只需访问其中一小部分数据时)

    • 可能增加冲突未命中(因为较大的块会占用更多缓存空间,导致其他数据无法缓存)

  • 增大缓存容量
    :更大的缓存可以减少容量未命中和冲突未命中,因为可以存储更多的数据。然而,增大缓存容量会带来访问时间的惩罚(较大的缓存通常访问速度会稍慢)。

  • 高关联度
    :增加缓存的相联度(如从1路变为2路或更高)可以减少冲突未命中,因为多个数据块可以存储在同一缓存组中,减少了因为地址冲突而导致的数据丢失。


    • 经验法则
      :容量为N/2的2路相联缓存的未命中率与容量为N的1路相联缓存相当,但高相联度会消耗更多能量。

3.6 更多 Caches 基础

  • L1缓存分为指令缓存和数据缓存,而L2和L3是统一缓存
    :在一级缓存(L1)中,将指令和数据分别存储在不同的缓存中,以提高访问效率;而在L2和L3级别,指令和数据共享同一个缓存空间,即为统一缓存。

  • L1/L2缓存层次结构可以是包容性、排他性或非包容性


    • 包容性
      :L2缓存包含所有L1缓存中的数据内容,这样如果L2有数据,L1肯定也有。这种结构便于管理,但会占用更多L2空间。

    • 排他性
      :L2缓存中的数据不在L1中,两个缓存不会重复存储相同数据,这种结构可以更有效地利用总的缓存空间。

    • 非包容性
      :L1和L2之间没有严格的包容关系,数据可以在L1或L2中单独存在。

  • 写操作时可以选择写分配或不写分配


    • 写分配
      (Write-Allocate):在写入缓存时,如果数据不在缓存中,会将其加载到缓存中后再写入。

    • 不写分配
      (Write-No-Allocate):如果数据不在缓存中,直接写到下一级缓存或主存,而不加载到缓存中。

  • 写操作时可以选择回写(Write-Back)或直写(Write-Through)


    • 回写
      (Write-Back):写入时只更新缓存中的数据,而不立即写回内存,只有当缓存行被替换时才写回内存,减少了内存总线的流量。

    • 直写
      (Write-Through):每次写入缓存时,同时更新内存,这样简化了数据一致性管理,但增加了内存访问流量。

  • 读操作优先于写操作,且写操作通常会进行缓冲
    :为了保证数据的快速读取,读操作通常具有更高优先级,而写操作会被缓冲,等待合适的时机再写入,避免影响读操作的效率。

  • L1缓存并行进行标签和数据访问,L2/L3缓存串行进行标签和数据访问


    • 在L1缓存中,标签(Tag)和数据的访问是并行的,以减少访问时间。

    • 在L2和L3缓存中,标签和数据访问是串行的,先检查标签是否命中,再决定是否读取数据,这种设计减少了电路的复杂性。

3.7 容忍罚失

在处理缓存未命中(cache miss)时,如何通过一些策略来减少等待带来的性能损失:

  • 乱序执行(Out of Order Execution)
    :在等待缓存未命中数据返回的时间内,处理器可以继续执行其他有用的工作,以提高整体的执行效率。这意味着处理器不需要因一次缓存未命中而停下来等待,而是可以继续处理其他指令。另外,乱序执行允许处理器同时处理多个缓存未命中事件,从而最大化资源利用率。
    • 多次缓存未命中(Multiple Cache Misses)
      :在这种情况下,缓存控制器需要能够跟踪多个未命中的请求,这就需要非阻塞缓存(Non-Blocking Cache)。非阻塞缓存允许处理器在等待一个未命中的数据时继续发出其他请求,而不是阻塞住当前的所有操作。
  • 硬件预取(Hardware Prefetching)
    :通过硬件将未来可能会访问的数据提前加载到预取缓冲区(Prefetch Buffer)中,以减少未来访问的等待时间。这种方法在程序具有
    空间或时间局部性
    时尤其有效,因为处理器可以预测到接下来可能需要的数据。
    • 激进预取(Aggressive Prefetching)
      :虽然预取可以减少等待时间,但如果预取过多,会增加总线(Bus)的争用压力,影响其他内存访问的性能。因此,预取的策略需要平衡数据加载和总线资源的竞争。

3.8 预取(Prefetching)

  • 硬件预取可以应用于任何缓存级别
    :预取是一种硬件技术,用来在数据实际被需要之前提前将其加载到缓存中,以减少等待时间。预取技术可以在任何缓存层次(如L1、L2、L3)中应用。
  • 预取会引起缓存污染
    :如果预取的数据并不被立即使用,可能会占用缓存中的位置,从而导致原本更有用的数据被替换掉,这种情况称为缓存污染。为避免污染,预取的数据通常放在一个单独的预取缓冲区中。这样,预取的数据不会与主缓存中的数据混合,但在访问缓存时需要并行查找这个缓冲区,以确保是否存在预取的数据。
  • 激进的预取增加“覆盖率”,但降低“准确率”
    :激进的预取意味着尽可能多地预取数据,这样可以覆盖更多的潜在数据需求,称为“增加覆盖率”。但是,这也可能导致许多预取的数据其实不会被用到,从而浪费了内存带宽,称为“降低准确率”。
  • 预取的时机很重要
    :预取数据的时机需要把握得当。预取必须足够提前,以便在数据需要时已经加载完毕,从而隐藏内存访问的延迟。但如果预取过早,数据可能在被使用前就因为缓存替换策略而被驱逐,导致缓存污染或资源浪费。

4. 缓存抽象(Abstraction)和结构(Structure)

4.1 缓存抽象和度量

image-20241108145559312

给一个地址来索引数据存储,tag store 回答寻找的地址是否在缓存中,它会发出信号Hit/miss,如果Hit,Data store会给出Data。

  • Cache hit rate = (# hits) / (# hits + # misses) = (# hits) / (# accesses)
  • Average memory access time (AMAT) = ( hit-rate * hit-latency ) + ( miss-rate * miss-latency )(include hit latency)
  • AMAT越小越好吗?

4.2 Block和缓存寻址

  • 内存在逻辑上被划分为固定大小的block
  • 每个数据块映射到缓存中的某个位置,该位置由地址中的index (索引) bits 或 set (组) bits 决定。
    • 用于索引标签和数据存储

image-20241108150340922

  • 缓存访问过程:
  1. 将索引输入 tag 和 data 存储,索引位位于地址中
  2. 检查tag store中的有效位
  3. 将地址中的 tag bits 与 tag store 中存储的标签进行比较
  • 如果数据块在缓存中(缓存命中),则存储的 tag 应
    有效valid
    ,并与数据块的 tag 相匹配

4.3 直接映射缓存:放置和访问

  • 假设字节可寻址内存:256bytes,8-byte blocks -> 32 blocks

  • 假设缓存: 64 bytes,8 blocks


    • 直接映射: 一个block只能前往一个location
  • 具有相同 index 的地址争夺相同位置


    • 导致冲突未命中

image-20241108151318826

  • 直接映射缓存:内存中映射到高速缓存中相同 index 的两个块不能同时出现在缓存中


    • 一个 index -> 一个 entry
  • 如果以
    交错方式访问
    的多个 block 映射到同一索引,则可能导致 0% 的命中率


    • 假设地址 A 和地址 B 的 index 位相同,但 tag 位不同
    • A, B, A, B, A, B, A, B, … -> 缓存 index 冲突
    • 所有访问都是
      冲突未命中

4.4 组关联性(Set Associativity)

4.4.1 2组关联

  • 在直接映射缓存中,地址 0 和 8 总是冲突的
  • 将一列 8 个 blocks 改为 2 列 4 个 blocks
  • index位减1,tag位加1

image-20241108152518390
image-20241108152638244

  • 组内关联存储器
    • 更好地适应冲突(更少的冲突未命中)
    • 更复杂、访问更慢、tag store更大

4.4.2 4组关联

image-20241108153556752

  • 冲突未命中的可能性更低
  • 更多 tag 比较器和更宽的数据复用器;更大的 tag

4.4.3 全关联

  • 完全关联式缓存
    • 一个数据块可放置在任何高速缓存位置

image-20241108153831092

4.4.4 关联性(和权衡)

  • 关联性程度:
    有多少 block 可以映射到同一个 index(或数据集)?

  • 更高的关联性


    • 更高的命中率
    • 更慢的缓存访问时间(命中延迟和数据访问延迟)
    • 更昂贵的硬件(更多的比较器)
  • 高关联度带来的收益递减

image-20241108154511095

4.5 降低未命中率(Miss Rate)的创新方法

  • Victim 缓存
  • 更好的替换策略——伪 LRU、NRU、DRRIP
    • 插入、提升、victim 选择
    • 预取、缓存压缩

4.5.1 Victim 缓存

  • 直接映射缓存会出现未命中情况,因为多个数据映射到同一位置
  • 处理器经常尝试访问最近丢弃的数据
    • 所有丢弃的数据都放在一个小的 victim 缓存中(4 或 8 个条目)
    • 在进入 L2 之前检查 victim 缓存
  • 可将其视为冲突最多的几个数据集的额外关联性

4.6 组关联缓存中的问题

  • 认为 set 中的每个block都有一个 "优先级"
    • 表示将该 block 保留在缓存中的重要性
  • 关键问题:如何确定/调整数据块的优先级?
  • 在一个 set 中有三个关键决定:
    • 插入、提升、剔除(替换)
  • 插入(Insertion):缓存填充时优先级会发生什么变化?
    • 在哪里插入进入的 block,是否插入 block
  • 提升(Promotion):缓存命中时优先级会发生什么变化?
    • 是否以及如何改变 block 的优先级
  • 驱逐/替换(Eviction/replacement):缓存未命中时优先级会发生什么变化?
    • 驱逐哪个 block 以及如何调整优先级

4.7 驱逐/更换策略

  • 在缓存 miss 时,要替换数据集中的哪个数据块?
    • 先替换任何无效 block
    • 如果所有 block 都有效,则参考替换策略
      • Random
      • FIFO
      • Least recently used (how to implement?) 最近最少使用(LRU)
      • Not most recently used 最近未使用
      • Least frequently used?使用频率最低
      • Least costly to re-fetch?重新获取的成本最低
      • Why would memory accesses have different cost?
      • Hybrid replacement policies 混合替换
      • Optimal replacement policy?

5. LRU

  • Idea
    :驱逐最近访问次数最少的区块

  • Problem:
    需要跟踪 block 的访问顺序

  • 问题:2路组关联缓存


    • 要完美实现 LRU,需要哪些条件?
      需要1bit记录访问顺序
  • 问题:4路组关联缓存


    • 完美实现 LRU 需要哪些条件?
    • 集合中的 4 个块可能有多少种不同的顺序?
      \(4\times 3\times 2\times 1 = 4! = 24\)
    • 需要多少位来编码块的 LRU 顺序?
      最少需要5bits
    • 确定 LRU victim 需要哪些逻辑?

IMG_0164(20241108-170423)

5.1 近似LRU

  • 大多数现代处理器都没有在高关联缓存中实现 "真正的 LRU"(也称为 "完美的 LRU")。


    • 因为真正的LRU是复杂的
    • LRU只是预测本地性的近似值(即不是最佳的高速缓存管理策略
  • Example:


    • NRU,非最近使用
    • DRRIP

5.2 驱逐/更换策略

  • NRU:
    集合中的每个 block 都有一个 bit;block 被访问时,该 bit 变为 0;如果所有 bit 都为 0,则将所有位变为 1;将 bit 设置为 1 的 block 驱逐出去

  • DRRIP:
    使用多个 NRU 位(3 位),将进入的 block 设置为高位,使其接近被驱逐;类似于将进入的 block 置于 LRU 列表的头部而非尾部附近

6. 缓存策略

6.1 Tag Store

Tag的组成可以有以下几部分:

  • Valid bit
  • Tag
  • Replacement policy bits(更换策略位)
  • Dirty bit(脏位)
    • 表示该数据为 write back 模式还是 write through模式

6.2 处理 Write 操作

6.2.1 写回模式(Write-back) vs. 写直达模式(Write-through)

  • 我们何时将缓存中修改过的数据写入下一级?


    • write through:
      写入时
    • write back:
      当 block 被驱逐时
  • Write-back(写回模式):
    更新缓存时,并不同步更新memory


    • 可以在驱逐前合并对同一 block 的多次写入
      • 有可能节省缓存级别之间的带宽,减少能耗
    • 需要在 tag store 中加入一个位,表明该数据块是 "脏的/修改过的"。
  • Write-through(写直达模式):
    CPU向cache写入数据时,同时向memory写


    • 简单
    • 所有级别的 Cache 都是最新的。
      一致性:
      缓存一致性更简单,因为无需检查接近处理器缓存的 Tag store 是否存在
    • 带宽更密集;不合并写入

6.2.2 写入未命中时分配(Allocate on write miss) vs. 写入未命中时不分配(No-allocate on write miss)

  • 我们是否会在
    写入未命中时分配缓存块(cache block)即是否会从缓存中驱逐其他缓存块

    • 写入未命中时分配(Allocate on write miss):是
    • 写入未命中时不分配(No-allocate on write miss):否
  • 写入未命中时分配(Allocate on write miss):将要写的地址所在的 block 先从 memory 读到 cache 中,然后写 cache
    • 可以
      合并写入
      ,而不是将每一个要写的数据每次都单独写入下一级
    • 更简单,因为
      写入未命中与读取未命中的处理方式相同
    • 需要传输整个缓存块(cache block)
  • 不分配(No-allocate):将要写的内容直接写回 memory
    • 如果写入 block 的局部性较低,则可节省缓存空间(可能会提高缓存命中率

6.3 分块(Subblocked/Sectored)缓存

  • 如果处理器在较短时间内写入整个数据块怎么办?是否有必要首先将数据块从内存带入缓存?

  • 为什么我们不能只写入数据块的一部分,即子数据块?


    • e.g. 写 64byte 中的 4byte
    • Problem:有效位和脏位与整个 64byte 相关联,而不是与每个 4byte 相关联
  • Idea:将一个 block 划分为子区块 subblocks(or 扇区sectors)


    • 每个 subblock(sector)都有单独的有效位和脏位
    • 一个请求只分配一个 subblocks(或一个 subblocks 子集)

image-20241109105032585

  • 优点:


    • 无需将整个缓存块 block 传输到缓存中(写入只需
      验证和更新一个 subblock
    • 将 subblocks 传输到缓存的自由度更大(缓存块 block 不需要完全在缓存中,只需要确定一次读取要传输多少个 subblocks?)
  • 缺点:


    • 设计更复杂;更多的有效位和脏位
    • 可能无法充分利用空间局部性

6.4 指令缓存(Instruction Cache) vs. 数据缓存(Data Cache)

  • 要分开(Separate)管理还是统一(Unified)管理

  • Unified的优缺点:


    • 动态共享缓存空间:不会出现静态分区(即独立的 Instruction 和 Data 缓存)可能出现的超额配置情况

    • 指令和数据会互相驱逐(即两者都没有充足的空间

    • Instruction 和 Data 在流水线的不同位置被访问。 我们应该把统一缓存放在哪里才能实现快速访问?
      这是大多数处理器使用单独(Separate)缓存的主要原因

  • 一级缓存几乎总是被拆分


    • 主要就是因为这个原因
      “Instruction 和 Data 在流水线的不同位置被访问。 我们应该把统一缓存放在哪里才能实现快速访问?”
  • 高级缓存几乎总是统一的


    • 这是因为
      “动态共享缓存空间:不会出现静态分区(即独立的 Instruction 和 Data 缓存)可能出现的超额配置情况”

6.5 流水线设计中的多级缓存

  • 一级缓存 L1(指令和数据):
    • 决定在很大程度上受周期时间的影响
    • 小、关联度低;延迟至关重要
    • Tag store 和 Data Store 并行访问
    • 指令和数据也是并行访问的
  • 二级缓存 L2:
    • 决策需要平衡命中率和访问延迟
    • 通常容量大,关联性强;延迟不那么重要
    • 以串行方式访问 Tag store 和 Data store
      • 延迟并不重要,重要的是命中率和节能
  • 缓存级别的串行与并行访问:
    • 串行:只有在一级缓存 miss 时才访问二级缓存
    • 第二层的访问权限与第一层不同
      • 第一层起到过滤器的作用(过滤一些时间和空间局部性)
      • 管理策略有所不同

img

7. 提高缓存性能

缓存参数与失误/命中率的关系

  • Cache 大小
  • Block 大小
  • 关联性
  • 驱逐策略
  • 插入策略

7.1 Cache 大小

  • 缓存大小:数据(不包括 tag)总容量


    • 越大越能利用时间局部性
    • 并非总是越大越好
  • 过大的缓存会对命中和未命中延迟产生不利影响


    • 越小越快 => 越大越慢
    • 访问时间可能会降低关键路径的性能
  • 缓存太小


    • 不能很好地利用时间局部性
    • 有用数据经常被替换
  • 工作集(working set)点:应用程序所引用的全部数据集可能存在于缓存中

image-20241109124348151

7.2 Block 大小

  • Block 大小是与地址 tag 关联的数据


    • 不一定是层级间的传输单位
      • Sub-blocking:一个 block 分为多个部分(每个部分都有 Valid/Dirty 位)
  • Block 太小


    • 不能很好地利用空间局部性
    • tag 开销较大
  • block 太大


    • 总 block 数过少
    • 时间局部性利用较少
    • 如果空间局部性不高,则会浪费缓存空间和带宽/能源

7.2.1 Large Block:Critical-Word and Subblocking

  • 大型缓存块可能需要很长时间才能填入缓存


    • 先填入缓存行
      关键字(Critical-Word)
    • 在完成填入之前重新启动缓存访问
  • 大型缓存块会浪费总线带宽


    • 将一个块划分为多个子块
    • 为每个子块分别设置有效位和脏位

7.3 关联性

同一索引(即组)中可以出现多少个block?

  • 更大的关联性
    • 更低的未命中率(减少冲突)
    • 更高的命中延迟和面积成本
  • 更小的关联性
    • 更低的成本
    • 更低的命中延迟
    • 对 L1 缓存尤为重要

image-20241109125250843

7.4 缓存 miss 的分类

  • 强制未命中
    • 对address(block)的首次引用总是导致未命中
    • 随后的引用应该命中,除非缓存块由于以下原因被移位
    • 解决方式:
      预取:预测哪些 block 即将需要
  • 容量未命中
    • 缓存太小,无法容纳所需的所有内容
    • 定义为即使在相同容量的完全关联缓存(具有最佳替换功能)中也会发生的缺失次数
    • 解决方式:
      • 更好地利用缓存空间:保留将被引用的 block
      • 软件管理:划分工作集和计算,使每个 "计算阶段 "都适合缓存
  • 冲突未命中
    • 定义为既非强制失误也非容量失误的任何失误
    • 解决方式:
      • 更多的关联性
      • 在不使缓存具有关联性的情况下获得更多关联性的其他方法
        • Victim cache
        • 更好的随机索引
        • 软件提示?

7.5 如何提高缓存性能

  • Reducing miss rate
    • 更多关联性
    • 关联性的替代方法/增强方法
    • victim 缓存、散列、伪关联性、倾斜关联性
    • 更好的替换/插入策略
    • 软件方法
  • Reducing miss latency or miss cost
    • 多级高速缓存
    • 关键字优先
    • 分块(subblocking)/扇区
    • 更好的替换/插入策略
    • 无阻塞高速缓存(多个高速缓存并行读取)
    • 每个周期多次访问
    • 软件方法
  • Reducing hit latency or hit cost


title: Nuxt.js 应用中的 listen 事件钩子详解
date: 2024/11/9
updated: 2024/11/9
author:
cmdragon

excerpt:
它为开发者提供了一个自由的空间可以在开发服务器启动时插入自定义逻辑。通过合理利用这个钩子,开发者能够提升代码的可维护性和调试能力。注意处理性能、错误和环境等方面的问题可以帮助您构建一个更加稳定和高效的应用。

categories:

  • 前端开发

tags:

  • Nuxt
  • 钩子
  • 开发
  • 服务器
  • 监听
  • 请求
  • 日志


image
image

扫描
二维码
关注或者微信搜一搜:
编程智域 前端至全栈交流与成长

目录

  1. 概述
  2. listen 钩子的详细说明
  3. 具体使用示例
  4. 应用场景
  5. 注意事项
  6. 总结

1. 概述

listen
钩子是在 Nuxt.js 开发服务器加载时被调用的生命周期钩子。它主要用于处理开发环境下服务器启动前后的自定义逻辑,例如监控请求动态或初始化配置。

2.
listen
钩子的详细说明

2.1 钩子的定义与作用

  • 定义

    listen
    是一个 Nuxt.js 钩子,允许开发者在开发服务器开始监听请求时执行自定义代码。
  • 作用
    :它使开发者能够在服务器启动时进行各种操作,例如初始化状态、设置事件监听器等。

2.2 调用时机

  • 执行环境
    :钩子在开发服务器启动后会被立刻调用。
  • 挂载时机
    :通常在 Nuxt 应用初始化的早期阶段,确保开发者的自定义代码能在请求处理之前执行。

2.3 参数说明

  • listenerServer
    :一个回调,用于访问服务器实例,允许执行对服务器的自定义操作。
  • listener
    :提供一个方法来设置对请求事件的监听。这通常用于监听 HTTP 请求。

3. 具体使用示例

3.1 示例:基本用法

以下是一个基本的
listen
钩子用法示例:

// plugins/serverListener.js
export default defineNuxtPlugin((nuxtApp) => {
  nuxtApp.hooks('listen', (listenerServer, listener) => {
    console.log('开发服务器已启动,准备监听请求...');

    listenerServer(() => {
      console.log('Nuxt 开发服务器已准备好接收请求!');
    });
  });
});

在这个示例中,我们定义了一个插件,在服务器启动时输出提示信息。这个钩子会在服务器准备好接受请求时被调用。

3.2 示例:请求日志记录

下面是一个示例,展示如何在接收到请求时记录请求的日志:

// plugins/requestLogger.js
export default defineNuxtPlugin((nuxtApp) => {
  nuxtApp.hooks('listen', (listenerServer, listener) => {
    console.log('开发服务器已经启动,准备监听请求...');

    listener((req, res) => {
      // 记录请求 URL 和方法
      console.log(`${req.method} 请求到: ${req.url}`);
      
      // 可以在这里添加处理请求的代码,如中间件
    });

    listenerServer(() => {
      console.log('服务器已经准备好,可以接受请求。');
    });
  });
});

4. 应用场景

4.1 初始化配置

描述
:在开发环境中,您可以在服务器启动时执行任何需要的配置任务。这包括设置数据库连接、加载环境变量等。

示例

// plugins/initConfig.js
export default defineNuxtPlugin((nuxtApp) => {
  nuxtApp.hooks('listen', async (listenerServer) => {
    console.log('初始化配置...');

    // 假设我们需要连接数据库
    await connectToDatabase();
    console.log('数据库连接成功!');
    
    listenerServer(() => {
      console.log('服务器已准备好,配置已初始化。');
    });
  });
});

// 示例的数据库连接函数
async function connectToDatabase() {
  // 模拟异步连接数据库操作
  return new Promise((resolve) => {
    setTimeout(() => {
      console.log('数据库连接成功!');
      resolve();
    }, 1000);
  });
}

4.2 请求监控

描述
:为了确保应用程序健康,您可能需要监控进入的每个 HTTP 请求。这对于调试和性能分析非常有用。

示例

// plugins/requestMonitor.js
export default defineNuxtPlugin((nuxtApp) => {
  nuxtApp.hooks('listen', (listenerServer, listener) => {
    
    listener((req, res) => {
      const startTime = Date.now();
      res.on('finish', () => {
        const duration = Date.now() - startTime;
        console.log(`[${req.method}] ${req.url} - ${duration}ms`);
      });
    });
    
    listenerServer(() => {
      console.log('请求监控已设置。');
    });
  });
});

4.3 动态中间件

描述
:通过
listen
钩子,您可以在应用程序运行时动态地设置中间件,这使得您的应用更加灵活。

示例

// plugins/dynamicMiddleware.js
export default defineNuxtPlugin((nuxtApp) => {
  nuxtApp.hooks('listen', (listenerServer, listener) => {
    
    listener((req, res, next) => {
      // 在特定条件下应用中间件
      if (req.url.startsWith('/admin')) {
        console.log('Admin 访问:', req.url);
      }
      
      // 调用下一个中间件
      next();
    });
    
    listenerServer(() => {
      console.log('动态中间件已设置。');
    });
  });
});

5. 注意事项

5.1 性能影响

描述
:在
listen
钩子中进行繁重的计算或耗时的操作,可能会显著影响服务器的启动时间。

示例

// plugins/performanceAware.js
export default defineNuxtPlugin((nuxtApp) => {
  nuxtApp.hooks('listen', (listenerServer, listener) => {
    console.log('服务器正在启动...');

    // 不要在这里做耗时操作
    setTimeout(() => {
      console.log('启动任务完成!');
    }, 5000); // 这将影响应用启动速度
  });
});

优化建议
:确保在执行耗时操作时使用异步方式,并考虑在服务器启动后进行初始化。

5.2 错误处理

描述
:在请求处理中添加错误处理逻辑是很重要的,以免因为未捕获的错误导致服务器崩溃。

示例

// plugins/errorHandling.js
export default defineNuxtPlugin((nuxtApp) => {
  nuxtApp.hooks('listen', (listenerServer, listener) => {
    
    listener((req, res) => {
      try {
        // 处理请求逻辑...
        // 假设发生错误
        throw new Error('模拟错误');
      } catch (error) {
        console.error('请求处理出错:', error);
        res.writeHead(500);
        res.end('服务器内部错误');
      }
    });
    
    listenerServer(() => {
      console.log('错误处理已设置。');
    });
  });
});

5.3 环境检测

描述
:确保
listen
钩子逻辑仅在开发环境中运行,以避免在生产环境中产生不必要的开销和安全问题。

示例

// plugins/envCheck.js
export default defineNuxtPlugin((nuxtApp) => {
  nuxtApp.hooks('listen', (listenerServer, listener) => {
    if (process.env.NODE_ENV !== 'development') {
      console.log('此逻辑仅在开发环境中运行。');
      return;
    }

    console.log('开发环境钩子逻辑正在运行...');
    
    listenerServer(() => {
      console.log('服务器已准备好,开发环境设置完成。');
    });
  });
});

6. 总结

listen
钩子在 Nuxt.js 开发过程中非常有用,它为开发者提供了一个自由的空间可以在开发服务器启动时插入自定义逻辑。通过合理利用这个钩子,开发者能够提升代码的可维护性和调试能力。注意处理性能、错误和环境等方面的问题可以帮助您构建一个更加稳定和高效的应用。

余下文章内容请点击跳转至 个人博客页面 或者 扫码关注或者微信搜一搜:
编程智域 前端至全栈交流与成长
,阅读完整的文章:
Nuxt.js 应用中的 listen 事件钩子详解 | cmdragon's Blog

往期文章归档:

先展示下最终效果:

第一步:先安装ollama,并配置对应的开源大模型。

安装步骤可以查看上一篇博客:

ollama搭建本地ai大模型并应用调用

第二步:需要注意两个配置,页面才可以调用
1)OLLAMA_HOST= "0.0.0.0:11434"
2)若应用部署服务器后想调用,需要配置:OLLAMA_ORIGINS=*
第三步:js流式调用大模型接口方法
async startStreaming(e) {if(e.ctrkey&&e.keyCode==13){this.form.desc+='\n';
}
document.getElementById(
"txt_suiwen").disabled="true";//如果已经有一个正在进行的流式请求,则中止它 if (this.controller) {this.controller.abort();
}

setTimeout(()
=>{this.scrollToBottom();
},
50);var mymsg=this.form.desc.trim();if(mymsg.length>0){this.form.desc='';this.message.push({
user:
this.username,
msg:mymsg
})
this.message.push({
user:
'GPT',
msg:
'',
dot:
''});//创建一个新的 AbortController 实例 this.controller = newAbortController();
const signal
= this.controller.signal;this.arequestData.messages.push({role:"user",content:mymsg});try{
const response
= await fetch('http://127.0.0.1:11434/api/chat', {
method:
'POST',
headers: {
'Content-Type': 'application/json'},
body:JSON.stringify(
this.arequestData),
signal
});
if (!response.body) {this.message[this.message.length-1].msg='ReadableStream not yet supported in this browser.';throw new Error('ReadableStream not yet supported in this browser.');
}

const reader
=response.body.getReader();
const decoder
= newTextDecoder();
let result
= '';this.message[this.message.length-1].dot='⚪';while (true) {
const { done, value }
=await reader.read();if(done) {break;
}
result
+= decoder.decode(value, { stream: true});//处理流中的每一块数据,这里假设每块数据都是完整的 JSON 对象 const jsonChunks = result.split('\n').filter(line =>line.trim());//console.log(result) for(const chunk of jsonChunks) {try{
const data
=JSON.parse(chunk);//console.log(data.message.content) this.message[this.message.length-1].msg+=data.message.content;
setTimeout(()
=>{this.scrollToBottom();
},
50);
}
catch(e) {//this.message[this.message.length-1].msg=e; //处理 JSON 解析错误 //console.error('Failed to parse JSON:', e); }
}
//清空 result 以便处理下一块数据 result = '';
}
}
catch(error) {if (error.name === 'AbortError') {
console.log(
'Stream aborted');this.message[this.message.length-1].msg='Stream aborted';
}
else{
console.error(
'Streaming error:', error);this.message[this.message.length-1].msg='Stream error'+error;
}
}
this.message[this.message.length-1].dot='';this.arequestData.messages.push({
role:
'assistant',//this.message[this.message.length-1].user,//"GPT", content: this.message[this.message.length-1].msg
})
setTimeout(()
=>{this.scrollToBottom();
},
50);

}
else{this.form.desc='';
}
document.getElementById(
"txt_suiwen").disabled="";
document.getElementById(
"txt_suiwen").focus();
}
}

vue完整代码如下:
<template> 
  <el-row:gutter="12"class="demo-radius">
    <divclass="radius":style="{
borderRadius: 'base'
}"
> <divclass="messge"id="messgebox"ref="scrollDiv"> <ul> <liv-for="(item, index) in message":key="index"style="list-style-type:none;"> <divv-if="item.user == username"class="mymsginfo"style="float:right"> <div> <el-avatarstyle="float: right;margin-right: 30px;background: #01bd7e;"> <!--{{ item.user.substring(0, 2) }}--> <img:alt="item.user.substring(0, 2)":src=userphoto/> </el-avatar> </div><divstyle="float: right;margin-right: 10px;margin-top:10px;width:80%;text-align: right;"> {{ item.msg }} </div> </div> <divv-else class="chatmsginfo" > <div> <el-avatarstyle="float: left;margin-right: 10px;"> {{ item.user }} </el-avatar> </div> <divstyle="float: left;margin-top:10px;width:80%;"> <imgalt="loading"v-if="item.msg == ''"class="loading"src="../../assets/loading.gif"/> <MdPreviewstyle="margin-top:-20px;":autoFoldThreshold="9999":editorId="id":modelValue=" item.msg + item.dot " /> <!--{{ item.msg }}--> </div> </div> </li> </ul> </div> <divclass="inputmsg"> <el-form:model="form" > <el-form-item> <el-avatarstyle="float: left;background: #01bd7e;margin-bottom: -44px;margin-left: 4px;z-index: 999;width: 30px;height: 30px;"> <imgalt="jin":src=userphoto/> </el-avatar> <el-inputid="txt_suiwen":prefix-icon="userphoto"resize="none"autofocus="true":autosize="{ minRows: 1, maxRows: 2 }"v-model="form.desc"placeholder="说说你想问点啥....按Enter键可直接发送"@keydown.enter.native.prevent="startStreaming($event)"type="textarea" /> </el-form-item> </el-form> </div> </div> </el-row> </template> <scriptsetup>import { MdPreview, MdCatalog } from'md-editor-v3';
import
'md-editor-v3/lib/preview.css';

const id
= 'preview-only';</script> <script>exportdefault{
data() {
return{
form: {
desc:
''},
message:[],
username:sessionStorage.name,
userphoto:sessionStorage.photo,
loadingtype:false,
controller:
null,
arequestData : {
model:
"qwen2",//"llama3.1", messages: []
}
}
},
mounted() {
},
methods: {
scrollToBottom() {
let elscroll
=this.$refs["scrollDiv"];
elscroll.scrollTop
=elscroll.scrollHeight+30},
clearForm(formName){
this.form.desc='';
},
async startStreaming(e) {
if(e.ctrkey&&e.keyCode==13){this.form.desc+='\n';
}
document.getElementById(
"txt_suiwen").disabled="true";//如果已经有一个正在进行的流式请求,则中止它 if(this.controller) {this.controller.abort();
}

setTimeout(()
=>{this.scrollToBottom();
},
50);varmymsg=this.form.desc.trim();if(mymsg.length>0){this.form.desc='';this.message.push({
user:
this.username, msg:mymsg
})
this.message.push({
user:
'GPT', msg:'',
dot:
''});//创建一个新的 AbortController 实例 this.controller= newAbortController();
const signal
= this.controller.signal;this.arequestData.messages.push({role:"user",content:mymsg});try{
const response
=await fetch('http://127.0.0.1:11434/api/chat', {
method:
'POST',
headers: {
'Content-Type':'application/json'},
body:JSON.stringify(
this.arequestData),
signal
});
if(!response.body) {this.message[this.message.length-1].msg='ReadableStream not yet supported in this browser.';throw newError('ReadableStream not yet supported in this browser.');
}

const reader
=response.body.getReader();
const decoder
= newTextDecoder();
let result
= '';this.message[this.message.length-1].dot='';while(true) {
const { done, value }
=await reader.read();if(done) {break;
}
result
+=decoder.decode(value, { stream:true});//处理流中的每一块数据,这里假设每块数据都是完整的 JSON 对象 const jsonChunks=result.split('\n').filter(line=>line.trim());//console.log(result) for(const chunk of jsonChunks) {try{
const data
=JSON.parse(chunk);//console.log(data.message.content) this.message[this.message.length-1].msg+=data.message.content;
setTimeout(()
=>{this.scrollToBottom();
},
50);
}
catch(e) {//this.message[this.message.length-1].msg=e; //处理 JSON 解析错误 //console.error('Failed to parse JSON:', e); }
}
//清空 result 以便处理下一块数据 result= '';
}
}
catch(error) {if(error.name=== 'AbortError') {
console.log(
'Stream aborted');this.message[this.message.length-1].msg='Stream aborted';
}
else{
console.error(
'Streaming error:', error);this.message[this.message.length-1].msg='Stream error'+error;
}
}
this.message[this.message.length-1].dot='';this.arequestData.messages.push({
role:
'assistant',//this.message[this.message.length-1].user,//"GPT", content:this.message[this.message.length-1].msg
})
setTimeout(()
=>{this.scrollToBottom();
},
50);

}
else{this.form.desc='';
}
document.getElementById(
"txt_suiwen").disabled="";
document.getElementById(
"txt_suiwen").focus();
}
},
beforeDestroy() {
//组件销毁时中止流式请求 if(this.controller) {this.controller.abort();
}
}
}
</script> <stylescoped>.radius{margin:0 auto; }.demo-radius .title{color:var(--el-text-color-regular);font-size:18px;margin:10px 0; }.demo-radius .value{color:var(--el-text-color-primary);font-size:16px;margin:10px 0; }.demo-radius .radius{min-height:580px;height:85vh;width:70%;border:1px solid var(--el-border-color);border-radius:14px;margin-top:10px; }.messge{width:96%;height:84%; /*border:1px solid red;*/margin:6px auto;overflow:hidden;overflow-y:auto; }.inputmsg{width:96%;height:12%; /*border:1px solid blue;*/border-top:2px solid #ccc;margin:4px auto;padding-top:10px; }.mymsginfo{width:100%;height:auto;min-height:50px; }::-webkit-scrollbar{width:6px;height:5px; }::-webkit-scrollbar-track{background-color:rgba(0, 0, 0, 0.2);border-radius:10px; }::-webkit-scrollbar-thumb{background-color:rgba(0, 0, 0, 0.5);border-radius:10px; }::-webkit-scrollbar-button{background-color:#7c2929;height:0;width:0px; }::-webkit-scrollbar-corner{background-color:black; } </style> <style>.el-textarea__inner{padding-left:45px;padding-top:.75rem;padding-bottom:.75rem; } </style>