从零构建深度学习推理框架-7 计算图的表达式

什么是表达式

表达式就是一个计算过程,类似于如下:

output_mid = input1 + input2
output = output_mid * input3

用图形来表达就是这样的。

但是在PNNX的表达式(Experssion Layer)中不是这个样子,而是以一种抽象得方式,替换掉输入张量改为@1,@2等等

所以上面的计算图也就变成了

add(@0,mul(@1,@2))

我们是希望把这个抽象的表达式变回到一个方便后端执行的计算过程(抽象的语法树来表达,在推理的时候转为逆波兰式)。

其中addmul表示我们上一节中说到的RuntimeOperator, @0@1表示我们上一节课中说道的RuntimeOperand. 这个抽象表达式看起来比较简单,但是实际上情况会非常复杂,我们给出一个复杂的例子:

add(add(mul(@0,@1),mul(@2,add(add(add(@0,@2),@3),@4))),@5)

这就要求我们需要一个鲁棒的表达式解析和语法树构建功能。

词法解析:

词法解析的目的就是将add(@0,mul(@1,@2))拆分为多个token,token依次为add ( @0 , mul等.代码如下:

enum class TokenType {
  TokenUnknown = -1,
  TokenInputNumber = 0,
  TokenComma = 1,
  TokenAdd = 2,
  TokenMul = 3,
  TokenLeftBracket = 4,
  TokenRightBracket = 5,
};

struct Token {
  TokenType token_type = TokenType::TokenUnknown;
  int32_t start_pos = 0; //词语开始的位置
  int32_t end_pos = 0; // 词语结束的位置

//比如add就是 start_pos = 0 , end_pos = 2
  
Token(TokenType token_type, int32_t start_pos, int32_t end_pos): token_type(token_type), start_pos(start_pos), end_pos(end_pos) {

  }
};

我们在TokenType中规定了Token的类型,类型有输入、加法、乘法以及左右括号等.Token类中记录了类型以及Token在字符串的起始和结束位置.

这样就把表达式变成了多个token的一个数组。

如下的代码是具体的解析过程,我们将输入(也就是:add(@0,mul(@1,@2)))存放在statement_中,首先是判断statement_是否为空, 随后删除表达式中的所有空格和制表符。

if (!need_retoken && !this->tokens_.empty()) {
    return;
  }

  CHECK(!statement_.empty()) << "The input statement is empty!";
  statement_.erase(std::remove_if(statement_.begin(), statement_.end(), [](char c) {
    return std::isspace(c);
  }), statement_.end());
  CHECK(!statement_.empty()) << "The input statement is empty!";

然后对于statement,我们遍历所有的表达式,要开始将这个statement拆成多个token啦!



for (int32_t i = 0; i < statement_.size();) {
    char c = statement_.at(i);
    if (c == 'a') {
      CHECK(i + 1 < statement_.size() && statement_.at(i + 1) == 'd')
              << "Parse add token failed, illegal character: " << c;
      CHECK(i + 2 < statement_.size() && statement_.at(i + 2) == 'd')
              << "Parse add token failed, illegal character: " << c;
      Token token(TokenType::TokenAdd, i, i + 3);
      tokens_.push_back(token);
      std::string token_operation = std::string(statement_.begin() + i, statement_.begin() + i + 3);
      token_strs_.push_back(token_operation);
      i = i + 3;
    } 
 }

char c就是当前的字符 如果这个等于a的话,那么由于我们的词法规定了以a开头的只有add,所以我们必须判断接下来的两个字符是不是'd','d',如果不是的话就报错,如果是的话就初始化一个新token保存。

同理:

else if (c == 'm') {
      CHECK(i + 1 < statement_.size() && statement_.at(i + 1) == 'u')
              << "Parse add token failed, illegal character: " << c;
      CHECK(i + 2 < statement_.size() && statement_.at(i + 2) == 'l')
              << "Parse add token failed, illegal character: " << c;
      Token token(TokenType::TokenMul, i, i + 3);
      tokens_.push_back(token);
      std::string token_operation = std::string(statement_.begin() + i, statement_.begin() + i + 3);
      token_strs_.push_back(token_operation);
      i = i + 3;
    } 

也只有mul这一种可能。

如果是一个操作数的话:

else if (c == '@') {
      CHECK(i + 1 < statement_.size() && std::isdigit(statement_.at(i + 1)))
              << "Parse number token failed, illegal character: " << c;
      int32_t j = i + 1;
      for (; j < statement_.size(); ++j) {
        if (!std::isdigit(statement_.at(j))) {
          break;
        }
      }
      Token token(TokenType::TokenInputNumber, i, j);
      CHECK(token.start_pos < token.end_pos);
      tokens_.push_back(token);
      std::string token_input_number = std::string(statement_.begin() + i, statement_.begin() + j);
      token_strs_.push_back(token_input_number);
      i = j;
    } 

那就是在@后只要是数字就一直读。读完之后组成新的token。

else if (c == ',') {
      Token token(TokenType::TokenComma, i, i + 1);
      tokens_.push_back(token);
      std::string token_comma = std::string(statement_.begin() + i, statement_.begin() + i + 1);
      token_strs_.push_back(token_comma);
      i += 1;
    } else if (c == '(') {
      Token token(TokenType::TokenLeftBracket, i, i + 1);
      tokens_.push_back(token);
      std::string token_left_bracket = std::string(statement_.begin() + i, statement_.begin() + i + 1);
      token_strs_.push_back(token_left_bracket);
      i += 1;
    } else if (c == ')') {
      Token token(TokenType::TokenRightBracket, i, i + 1);
      tokens_.push_back(token);
      std::string token_right_bracket = std::string(statement_.begin() + i, statement_.begin() + i + 1);
      token_strs_.push_back(token_right_bracket);
      i += 1;
    } else {
      LOG(FATAL) << "Unknown  illegal character: " << c;
    }

其他输入符也是如此。要是不再我们所列的项当中就报错。

这样我们就可以得到一个抽象的语法树。

语法解析:

语法解析的过程是递归向下的,定义在Generate_函数中.

 通过这个语法树中序遍历left、right就可以得到具体的一个计算的过程。0 mul 1 add 0 mul 1

我们这里用一个例子来讲解:

add(@0,@1)这个例子.输入到Generate_函数中, 是一个token数组.

Generate_数组首先检查第一个输入是否为add,mul或者是input number中的一种.

CHECK(current_token.token_type == TokenType::TokenInputNumber|| 
current_token.token_type == TokenType::TokenAdd || current_token.token_type == TokenType::TokenMul);

那这里为什么不判断第一个不是left bracket token(左括号)或)(右括号)呢?

因为这个一般只会是以add,mul或者光一个数字@0。

第一个输入add,所以我们需要判断其后是否是left bracket来判断合法性, 如果合法则构建左子树.

else if (current_token.token_type == TokenType::TokenMul || current_token.token_type == TokenType::TokenAdd) {
    std::shared_ptr<TokenNode> current_node = std::make_shared<TokenNode>();//组枝起来一个节点
    current_node->num_index = -int(current_token.token_type);

    index += 1;//到左括号 因为add之后的的token一定到左括号 不对就报错
    CHECK(index < this->tokens_.size());
    CHECK(this->tokens_.at(index).token_type == TokenType::TokenLeftBracket);

    index += 1;//左括号之后一定是一个操作数
    CHECK(index < this->tokens_.size());
    const auto left_token = this->tokens_.at(index);
    //token当前是@0这个token
    if (left_token.token_type == TokenType::TokenInputNumber
        || left_token.token_type == TokenType::TokenAdd || left_token.token_type == TokenType::TokenMul) {
//递归调用
      current_node->left = Generate_(index);
    }

处理下一个token, 构建左子树.

if (current_token.token_type == TokenType::TokenInputNumber) {
    uint32_t start_pos = current_token.start_pos + 1;
    uint32_t end_pos = current_token.end_pos;
    CHECK(end_pos > start_pos);
    CHECK(end_pos <= this->statement_.length());
    const std::string &str_number =
        std::string(this->statement_.begin() + start_pos, this->statement_.begin() + end_pos);
    return std::make_shared<TokenNode>(std::stoi(str_number), nullptr, nullptr);

  }

递归进入左子树后,判断是TokenType::TokenInputNumber则返回一个新的TokenNode到add token成为左子树.

检查下一个token是否为逗号,也就是在add(@0,@1)的@0是否为,

CHECK(this->tokens_.at(index).token_type == TokenType::TokenComma);

    index += 1;
    CHECK(index < this->tokens_.size());

下一步是构建add token的右子树

index += 1;
    CHECK(index < this->tokens_.size());
    const auto right_token = this->tokens_.at(index);
    if (right_token.token_type == TokenType::TokenInputNumber
        || right_token.token_type == TokenType::TokenAdd || right_token.token_type == TokenType::TokenMul) {
      current_node->right = Generate_(index);
    } else {
      LOG(FATAL) << "Unknown token type: " << int(left_token.token_type);
    }

    index += 1;
    CHECK(index < this->tokens_.size());
    CHECK(this->tokens_.at(index).token_type == TokenType::TokenRightBracket);
    return current_node;
current_node->right = Generate_(index); /// 构建add(@0,@1)中的右子树

Generate_(index)递归进入后遇到的token是@1 token,因为是Input Number类型所在构造TokenNode后返回.

if (current_token.token_type == TokenType::TokenInputNumber) {
    uint32_t start_pos = current_token.start_pos + 1;
    uint32_t end_pos = current_token.end_pos;
    CHECK(end_pos > start_pos);
    CHECK(end_pos <= this->statement_.length());
    const std::string &str_number =
        std::string(this->statement_.begin() + start_pos, this->statement_.begin() + end_pos);
    return std::make_shared<TokenNode>(std::stoi(str_number), nullptr, nullptr);

  }

之后检查右括号在不在:

index += 1;
    CHECK(index < this->tokens_.size());
    CHECK(this->tokens_.at(index).token_type == TokenType::TokenRightBracket);
    return current_node;
  } else {
    LOG(FATAL) << "Unknown token type: " << int(current_token.token_type);
  }

至此, add语句的抽象语法树构建完成.

struct TokenNode {
  int32_t num_index = -1;
  std::shared_ptr<TokenNode> left = nullptr;
  std::shared_ptr<TokenNode> right = nullptr;
  TokenNode(int32_t num_index, std::shared_ptr<TokenNode> left, std::shared_ptr<TokenNode> right);
  TokenNode() = default;
};

在上述结构中, left存放的是@0表示的节点, right存放的是@1表示的节点

我们再弄一个更复杂一些的例子:

add(mul(@0,@1),@2)

  • add
  • (
  • mul
  • (
  • @0
  • ,
  • @1
  • )
  • ,
  • @2
  • )
  • index = 0, 当前遇到的tokenadd, 调用层为1
  • index = 1, 根据以上的流程,我们期待add token之后的tokenleft bracket, 否则就报错. 调用层为1
  • 开始递归调用,构建add的左子树.从层1进入层2
  • index = 2, 遇到了mul token. 调用层为2.
  • index = 3, 根据以上的流程,我们期待mul token之后的token是第二个left bracket. 调用层为2.
  • 开始递归调用用来构建mul token的左子树.
  • index = 4, 遇到@0,进入递归调用,进入层3, 但是因为操作数都是叶子节点,构建好之后就直接返回了,得到mul token的左子节点.放在mul tokenleft指针上.
  • index = 5, 我们希望遇到一个逗号,否则就报错mul(@0,@1)中中间的逗号.调用层为2.
  • index = 6, 遇到@2,进入递归调用,进入层3, 但是因为操作数是叶子节点, 构建好之后就直接返回到2,得到mul token的右子节点.
  • index = 7, 我们希望遇到一个右括号,就是mul(@1,@2)中的右括号.调用层为2.
  • 到现在为止mul token已经构建完毕,返回形成add token的左子节点,add token的left指针指向构建完毕的mul树. 返回到调用层1.
    ...
  • add token开始构建right token,但是因为@2是一个输入操作数,所以直接递归就返回了,至此得到add的右子树,并用right指针指向.

这个东西最厉害的地方就在于,括号里面一定是一个新的节点!

Experssion Layer的实现(如何实现@0 + @1):

Expression Operator的定义

class ExpressionOp : public Operator {
 public:
  explicit ExpressionOp(const std::string &expr);
  std::vector<std::shared_ptr<TokenNode>> Generate();

 private:
  std::unique_ptr<ExpressionParser> parser_;
  std::vector<std::shared_ptr<TokenNode>> nodes_;
  std::string expr_;
};

其中expr_表示表达式字符串, nodes_表示经过逆波兰变换之后得到的节点.

Expression Layer的定义

class ExpressionLayer : public Layer {
 public:
  explicit ExpressionLayer(const std::shared_ptr<Operator> &op);
  void Forwards(const std::vector<std::shared_ptr<Tensor<float>>> &inputs,
                std::vector<std::shared_ptr<Tensor<float>>> &outputs) override;
 private:
  std::unique_ptr<ExpressionOp> op_;
};

初始化Expression Layer

ExpressionLayer::ExpressionLayer(const std::shared_ptr<Operator> &op) : Layer("Expression") {
  CHECK(op != nullptr && op->op_type_ == OpType::kOperatorExpression);
  ExpressionOp *expression_op = dynamic_cast<ExpressionOp *>(op.get());

  CHECK(expression_op != nullptr) << "Expression operator is empty";
  this->op_ = std::make_unique<ExpressionOp>(*expression_op);
}

 

Expression Layer中的输入排布

 

Expression Layer的输入中, 多个输入依次排布. 如果batch_size的大小为4, 则上图中input1中的元素数量为4, input2的元素数量也为4. 换句话说, input1中的数据都来源于操作数1(operand 1), input2中的数据都来源于操作数2(operand 2).

将数据存放到input1input2的实现如下:

int batch_size = 4;
  for (int i = 0; i < batch_size; ++i) {
    std::shared_ptr<ftensor> input = std::make_shared<ftensor>(3, 224, 224);
    input->Fill(1.f);
    inputs.push_back(input);
  }

  for (int i = 0; i < batch_size; ++i) {
    std::shared_ptr<ftensor> input = std::make_shared<ftensor>(3, 224, 224);
    input->Fill(2.f);
    inputs.push_back(input);
  }

inputs被分为两段, 前半段存放input1, 前半段的长度为4. 后半段存放input2, 后半段的长度为4.

计算的结果存放在outputs, 8个输入数据两两相加, 最后的输出数据大小等于4.

Expression Layer的计算过程

数据排布

 

第一个例子

 

已知有如上的数据存储排布, 在本节中我们将讨论如何根据现有的数据完成add(@0,@1)计算. 可以看到每一次计算的时候, 都以此从input1input2中取得一个数据进行加法操作, 并存放在对应的输出位置.

第二个例子

下图的例子展示了对于三个输入,mul(add(@0,@1),@2)的情况:

 

每次计算的时候依次从input1, input2input3中取出数据, 并作出相应的运算, 并将结果数据存放于对应的output中.

操作数处理的代码实现

ExpressionLayer::Forward函数中, 首先检查输入是否为空, 并初始化outputs数组中的元素.

CHECK(!inputs.empty());

  const uint32_t batch_size = outputs.size();
  CHECK(batch_size != 0);

  for (uint32_t i = 0; i < batch_size; ++i) {
    CHECK(outputs.at(i) != nullptr && !outputs.at(i)->empty());
    outputs.at(i)->Fill(0.f);
  }

  CHECK(this->op_ != nullptr && this->op_->op_type_ == OpType::kOperatorExpression);
  std::stack<std::vector<std::shared_ptr<Tensor<float>>>> op_stack;
  const std::vector<std::shared_ptr<TokenNode>> &token_nodes = this->op_->Generate();

this->op_->Generate(); 获得的是逆波兰表达式.

for (const auto &token_node : token_nodes) {
    if (token_node->num_index >= 0) {
      uint32_t start_pos = token_node->num_index * batch_size;
      std::vector<std::shared_ptr<Tensor<float>>> input_token_nodes;
      for (uint32_t i = 0; i < batch_size; ++i) {
        CHECK(i + start_pos < inputs.size());
        input_token_nodes.push_back(inputs.at(i + start_pos));
      }
      op_stack.push(input_token_nodes);
    }
  }

依次遍历逆波兰表达式, 如果当前的op遇到的是一个操作数, 例如@0或者@1. 就将他们一个批次的数据(input_token_nodes)全部读取出来, 并临时存放到栈op_stack中.

 

举个例子, 对于input1就将input1中所有的数据读取出来并存放到input_token_nodes中, 再将input_token_nodes这一个批次的数据放入到栈中.

根据输入的逆波兰式@0,@1,add,遇到的第一个节点是操作数是@0, 所以栈op_stack内的内存布局如下:

 

当根据顺序遇到第二个节点(op)的时候, 操作数@1的时候, 再将inputs中的操作数读取出来并存放到input_token_nodes中, 再将input_token_nodes这一个批次的数据放入到栈中.

 

运算符处理的代码实现

const int32_t op = token_node->num_index;
      CHECK(op_stack.size() >= 2) << "The number of operand is less than two";
      std::vector<std::shared_ptr<Tensor<float>>> input_node1 = op_stack.top();

      CHECK(input_node1.size() == batch_size);
      op_stack.pop();

      std::vector<std::shared_ptr<Tensor<float>>> input_node2 = op_stack.top();
      CHECK(input_node2.size() == batch_size);
      op_stack.pop();

当节点(op)类型为操作符号的时候, 首先弹出栈(op_stack)内的两个批次操作数, 对于如上的情况input_node1分别存放input1...4, input_node2分别存放input5...8.

CHECK(input_node1.size() == input_node2.size());
      std::vector<std::shared_ptr<Tensor<float>>> output_token_nodes(batch_size);
      for (uint32_t i = 0; i < batch_size; ++i) {
        if (op == -int(TokenType::TokenAdd)) {
          output_token_nodes.at(i) = ftensor::ElementAdd(input_node1.at(i), input_node2.at(i));
        } else if (op == -int(TokenType::TokenMul)) {
          output_token_nodes.at(i) = ftensor::ElementMultiply(input_node1.at(i), input_node2.at(i));
        } else {
          LOG(FATAL) << "Unknown operator type: " << op;
        }
      }
      op_stack.push(output_token_nodes);

当获取大小长度为batch_sizeinput_node1input_node2后, 流程在for(int i = 0...batch_size)中对两个输入进行两两操作, 操作类型定义于当前的op中. 对于逆波兰式@0,@1,add, 在如上处理完两个输入节点之后,当前的节点类型是add.

 

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/70145.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

韩顺平Linux基础篇

一、课程内容 二、Linux应用领域 一、Linux使用在哪些地方 Linux最强应用&#xff1a;服务器 三、Linux概述 三、Linux和Unix的关系 五、VM和Linux的安装 基本说明 学习Linux需要一个环境&#xff0c;我们需要创建一个虚拟机&#xff0c;然后再虚拟机上安装一个Centos系统来学…

【多重信号分类】超分辨率测向方法——依赖于将观测空间分解为噪声子空间和源/信号子空间的方法具有高分辨率(HR)并产生准确的估计(Matlab代码实现)

【多重信号分类】超分辨率测向方法——依赖于将观测空间分解为噪声子空间和源/信号子空间的方法具有高分辨率&#xff08;HR&#xff09;并产生准确的估计&#xff08;Matlab代码实现&#xff09; &#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️…

检测文本是否由AI生成,GPT、文心一言等均能被检测

背景 目前很多机构推出了ChatGPT等AI文本检测工具&#xff0c;但是准确率主打一个模棱两可&#xff0c;基本和抛硬币没啥区别。 先说结论&#xff0c;我们对比了常见的几款AI检测工具&#xff0c;copyleaks检测相比较而言最准确。 检测文本 AI文本片段1 来源&#xff1a;G…

【Maven】常用命令、插件管理、私服nexus

【Maven】常用命令、插件管理、私服nexus 常用命令 插件管理 私服nexus Nexus3 配置私服 项目pom中的配置 发布时区分正式版、快照版 常用命令 Maven提供了一系列常用命令&#xff0c;用于构建、测试和管理项目。以下是一些常用的Maven命令示例&#xff1a; mvn clean:…

圆圈中最后剩下的数字——剑指 Offer 62

文章目录 题目描述解法一题目描述 解法一 class Solution

Cobbler自定义yum源

再次了解下Cobbler的目录结构&#xff1a; 在/var/www/cobbler/ks_mirror目录下存放的是所有的镜像。 存放的是仓库镜像&#xff1a; 在/var/lib/cobbler/kickstarts目录下是存放的所有的kickstarts文件。 再有就是/etc/cobbler这个目录&#xff1a; [rootvm1 loaders]# cd /…

ad+硬件每日学习十个知识点(26)23.8.6 (DCDC的降压电路、升压电路、降压-升压电路,同步整流,选型考虑同步、隔离)

文章目录 1.DCDC的降压原理2.DCDC的升压原理3.DCDC的升压和降压原理4.什么是肖特基二极管造成的死区电压&#xff1f;5.MOS管有死区电压么&#xff1f;6.DCDC的同步整流&#xff08;用MOS管取代整流二极管&#xff0c;避免死区电压的影响&#xff09;7.DCDC选型——同步与非同步…

kube-prometheus 使用 blackbox-exporter 进行icmp 监控

安装kube-prometheus 后默认在monitoring namespace中有创建 blackbox-exporter deployment。但默认没有icmp的module配置&#xff0c;无法执行ping探测。因为即使有icmp module&#xff0c;默认配置也是无法执行ping探测的&#xff08;这篇文章要解决的就是这个问题&#xff0…

数据结构-带头双向循环链表的实现

前言 带头双向循环链表是一种重要的数据结构&#xff0c;它的结构是很完美的&#xff0c;它弥补了单链表的许多不足&#xff0c;让我们一起来了解一下它是如何实现的吧&#xff01; 1.节点的结构 它的节点中存储着数据和两个指针&#xff0c;一个指针_prev用来记录前一个节点…

OpenStack-Y版安装部署

OpenStack-Y版安装部署 目录 OpenStack-Y版安装部署 1、环境准备 1.1 环境简介1.2 配置hosts解析(所有节点)1.3 配置时间同步1.4 安装openstack客户端(控制节点执行)1.5 安装部署MariaDB(控制节点执行)1.6 安装部署RabbitMQ(控制节点执行)1.7 安装部署Memcache(控制节点执行)2、…

JS 解决鼠标悬浮显示弹窗 迅速离开时弹窗显示到其他位置的延迟问题

解决该问题的思路就是&#xff0c;判断当前鼠标的位置是否在某个div上&#xff0c;如果在这个div上则取消显示悬浮弹窗消息。 首先监听鼠标的移动事件 鼠标移动时判断是否在div里面进行移动了 clientX表示鼠标X的位置 client Y表示鼠标Y的位置 拿到要判断的div元素 获取off…

MySQL表的增删查改

目录 一&#xff0c;新增 二&#xff0c;查询 2.1 全列查询 2.2 指定列查询 2.3 查询字段为表达式 2.4 别名 - as 2.5 去重 - distinct 2.6 排序 - order by 2.7 条件查询 - where 2.8 分页查询 - limit 三&#xff0c;修改 - update 四&#xff0c;删除 - delete 一…

学习51单片机怎么开始?

学习的过程不总是先打好基础&#xff0c;然后再盖上层建筑&#xff0c;尤其是实践性的、工程性很强的东西。如果你一定要先全面打好基础&#xff0c;再学习单片机&#xff0c;我觉得你一定学不好&#xff0c;因为你的基础永远打不好&#xff0c;因为基础太庞大了&#xff0c;基…

TOMCAT的多实例部署和动静分离

tomcat的多实例 动静分离 排错小工具&#xff1a; telnet工具&#xff1a;yum -y install telnet telnet工具用于测试端口是否正常 telnet 20.0.0.101 80Tomcat多实例部署&#xff1a; 一台服务器上有多个tomcat的服务 1.安装好 jdk 2.安装 tomcat cd /opt tar zxvf apache-…

c语言——完数的计算

完数即所有因子之和等于其本身值 列入&#xff0c;28124714&#xff0c;28所有的因子为1&#xff0c;2&#xff0c;4&#xff0c;7&#xff0c;14 而这五个因子之和恰好也是28. //完数的计算 /*完数即所有因子之和等于其本身值 列入&#xff0c;28124714&#xff0c;28所有的…

【IMX6ULL驱动开发学习】02.hello驱动程序之cdev注册字符设备驱动程序和设置次设备号

目录 ​编辑 一、register_chrdev 二、解决方法 2.1 alloc_chrdev_region函数&#xff1a;注册一系列字符设备编号 2.2 cdev_init函数&#xff1a;初始化cdev结构体 2.3 cdev_add函数&#xff1a;将字符设备添加到系统中 三、驱动程序 一、register_chrdev major reg…

创建CREATE_STAT_TABLE 统计信息表在达梦和oracle中的使用

达梦 创建CREATE_STAT_TABLE 统计信息表 PROCEDURE CREATE_STAT_TABLE ( STATOWN VARCHAR(128), STATTAB VARCHAR(128), TABLESPACE VARCHAR(128) DEFAULT NULL, GLOBAL_TEMPORARY BOOLEAN DEFAULT FALSE ); 创建普通表的对应系统表的列名字段包括以下&#xff1a; OWNER TABL…

大数据Flink(六十一):Flink流处理程序流程和项目准备

文章目录 Flink流处理程序流程和项目准备 一、Flink流处理程序的一般流程

Fortinet数据中心防火墙及服务ROI超300%!Forrester TEI研究发布

近日&#xff0c;专注网络与安全融合的全球网络安全领导者 Fortinet&#xff08;NASDAQ&#xff1a;FTNT&#xff09;联合全球知名分析机构Forrester发布总体经济影响独立分析报告&#xff0c;详细阐述了在企业数据中心部署 FortiGate 下一代防火墙&#xff08;NGFW&#xff09…

C语言中几种常见数据类型所占字节数

**16位编译器&#xff1a; char/unsigned char &#xff1a;1字节 char &#xff1a;2字节 short int&#xff1a;2字节 int/unsigned int&#xff1a;2字节 long int&#xff1a;4字节 float&#xff1a;4字节 double&#xff1a;8字节* 32位编译器&#xff1a; *char/unsi…