2024年4月

费用分摊计算是个十分复杂的工作,尤其是在多云的环境下,尤其是公司严谨需要按项目分摊成本的情况下,尤其是一个项目还分布在不同云厂商不同云账户的场景下,尤其是云厂商toB的业务模式还会有超级复杂的资源折扣加持下,想要把帐算清楚难度巨大

我们每个月都需要计算项目成本,每到这个时候就很头疼,会花很多的时间在对账算账上。为了优化这个问题,我们上线了账单系统,不仅可以清晰明了以可视化的方式来查看账单,更为重要的是通过账单系统,还能方便快速的发现一些账务上面的问题,帮助公司优化成本管理。这篇文章就简单的介绍一下账单系统

为了避免引起误会,以下所有资源和账单无论是文章中提到还是截图中截取的都是
脱敏模拟数据
,并不代表真实消费情况,各位不必当真

系统介绍

尽管各家云厂商基本都可以按照项目来划分资源,但各家云厂商对项目这个维度的费用账单却并不完善,并不是所有的资源都可以按照项目维度去计算费用,这无疑增加了我们按项目汇总资源消费的难度。好在我们
多云系统
汇总了所有云上资源,同时也有这些资源的所属项目及关联关系,这让我们的账单系统好做很多,可以针对不同的云厂商采用不同的费用汇算方式,以达到最终费用计算分摊的目的

不同云厂商需要采用不同的费用计算方式,这缘于不同云厂商对费用对支持力度不一样导致的,例如腾讯云几乎所有的资源都支持按标签计费,这样我们只需要给不同的资源打上对应的项目标签就能很方便的算出项目费用,而阿里云对于标签的支持却不完善,所以需要针对不同云厂商找到适合的费用汇算方式,这个是另外一个议题,后续文章再讲,这一篇重点介绍下账单系统

总体概览

首先我们需要知道每个月的消费总额,所以做了这个费用总体概览页面,依次展示了记账周期内的费用总额、最近半年内的整体消费趋势,这些费用所分布的云账户的柱状图和饼状图,以及这些费用所分布的项目的饼状图和柱状图

这个页面可以清晰的知道总消费情况,以及消费趋势,同时也能对账户消费和项目消费情况有一个整体的了解,图表的形式确实比数字要好太多,高低不同的柱子能立刻看得出来究竟哪个月份消费最高,大小不同的圆形切块也能马上知道究竟哪个项目花费最大

账单一般都是按月汇总的,点击右上角可以切换月份查看。这里需要注意的是账单汇总数据要统一币种,云厂商的账单国内账户一般人民币结算,国际账户美元结算,在展示汇总数据时要统一币种,这中间涉及到一个汇率计算

项目概览

每个项目owner要按项目汇总计算消费情况,所以按项目去展示账单对每个owner来说更为重要,项目概览页面就是展示某个项目下的整体消费概览,包含当前账单概览展示的项目名称、项目月度消费总额、项目分布的云账户数量,近一年的项目消费趋势,项目所分布的云账户消费的柱状图和饼状图,以及各个云账户下具体消费资源分布情况

点击左上角可以切换项目,查看不同项目的整体消费情况,把握项目消费趋势,洞察项目资源分布

项目详情

点击消费数据Card,可以查看项目消费详情,项目账单详情页面除了展示项目相关的汇总数据外,更为重要的是会展示当前项目下的详细账单,包含消费月份,账单云账户,消费资源ID、名称、类型及配置,还有原价、折扣及实付价格

如果项目下资源不多的话,账单条数不会很多,每个月过一遍很有必要,很容易找到一些异常。云厂商的计费十分复杂,根据我们的使用经验来说,它们搞错的情况也非常多,多收钱的概率也很大,尤其是有折扣的情况下,我们就通过详细账单发现了许多计费问题,省下不少钱

账户列表

除了按项目汇总计算外,还可以按账户单独查看,账户列表页面可以展示计费周期内的所有云账户以及消费总额

因为这里是按账户统计的,每个账户的费用结算币种固定,所以直接展示结算币种,避免汇率计算,更加准确

账户概览

点击账户Card可以查看账户消费整体概览,包含当前云账户名称、账户月度消费总额、账户里使用的产品数量、地域数量、有多少个项目用到了当前账户内的资源,近一年的账户消费趋势,账户下的项目消费柱状图和饼状图、账户下的产品消费柱状图和饼状图,以及账户下不同地域消费的柱状图和饼状图

账户下的消费情况也更加清晰

账户详情

当然除了账户费用概览外,还会有给予账户的费用详情,点击账户概览页面的消费数据Card即可进入账户账单详情页,在这里除了账户相关汇总数据外,还能展示当前账户下的详细账单,包含消费月份,所属项目,消费资源ID、名称、类型及配置,还有原价、折扣及实付价格

有概览有详情,查看起来十分方便。在这里方便对比不同项目相同资源或相同资源不同配置的收费情况,仔细看看,又能看出很多问题,加入优化清单

写在最后

账单系统上线后,小伙伴们对线上账单进行了详细的校对,发现了很多问题,让我有种运维也可以作为创收部门存在的错觉

从发现的问题来看,云平台的计费确实十分复杂,云平台也不可全信,或许它们自己都没办法很准确的算清楚各种账单。所以推荐使用云的小伙伴,都仔细看看云平台给出的账单,大概率会有意想不到的收获

至此运维自动化平台又下一城,包含了
多云系统

作业系统

任务系统

监控系统

容器系统

域名系统
等等实用且好用的子系统,涵盖了日常运维工作的方方面面

实现的效果

最后的实现效果

如果你对此感兴趣,可以接着往下阅读。

实现过程

绘制矩形

比如说我想绘制一个3行4列的表格:

 private void Button_Click_DrawRect(object sender, RoutedEventArgs e)
 {
     int Row = 3;
     int Col = 4;
     
     for(int i = 0; i < Row; i++)
     {
         for(int j = 0; j< Col; j++) 
         {
             // 添加矩形
             System.Windows.Shapes.Rectangle rectangle = new System.Windows.Shapes.Rectangle
             {
                 Width = 50,
                 Height = 50,
                 Stroke = System.Windows.Media.Brushes.Blue,

                 // 设置填充颜色为透明色
                 Fill = System.Windows.Media.Brushes.Transparent,
                 StrokeThickness = 1
             };
          
             Canvas.SetLeft(rectangle, 80 + 50 * j);
             Canvas.SetTop(rectangle, 50 + 50 * i);
           
             myCanvas1.Children.Add(rectangle);
            
         }
        
       
     }

实现的效果:

image-20240418110949637

现在又想画4行3列的表格了,只需修改这里:

int Row = 4;
int Col = 3;

实现的效果:

image-20240418666666330852

为每个单元格添加信息

绘制了单元格之后,我们想要在单元格中添加它所在的行与列的信息。

在绘制矩形后面添加:

 // 在矩形内部添加文字
 TextBlock textBlock = new TextBlock
 {
     Text = i + "-" + j,
     Foreground = System.Windows.Media.Brushes.Black,
     FontSize = 12
 };

 Canvas.SetLeft(textBlock, 80 + 50 * j + 10);
 Canvas.SetTop(textBlock, 50 + 50 * i + 10);

 myCanvas1.Children.Add(textBlock);

现在实现的效果如下所示:

image-20240419084117023

让每个单元格可以被选中与取消选中

我们设定鼠标左键点击表示选中,鼠标右键点击表示取消选中,选中之后,单元格边框会变红,取消选中后又恢复原来的颜色。

为每个单元格添加鼠标点击事件处理程序:

 // 添加鼠标事件处理器,左键点击表示选中
 rectangle.MouseLeftButtonDown += Rectangle_MouseLeftButtonDown;

 // 添加鼠标事件处理器,右键点击表示取消选中
 rectangle.MouseRightButtonDown += Rectangle_MouseRightButtonDown;

鼠标点击事件处理程序:

 // 鼠标事件处理程序,左键点击表示选中
 private void Rectangle_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
 {   
     System.Windows.Shapes.Rectangle? rectangle = sender as System.Windows.Shapes.Rectangle;
     if (rectangle != null)
     {
         // 改变矩形的颜色以表示它被选中
         rectangle.Stroke = System.Windows.Media.Brushes.Red;             
     }
 }

  // 鼠标事件处理器,右键点击表示选中
  private void Rectangle_MouseRightButtonDown(object sender, MouseButtonEventArgs e)
  {   
      System.Windows.Shapes.Rectangle? rectangle = sender as System.Windows.Shapes.Rectangle;
      if (rectangle != null)
      {
          // 改变矩形的颜色以表示它被取消选中
          rectangle.Stroke = System.Windows.Media.Brushes.Blue;
                     
      }
  }

现在查看实现的效果:

点击单元格改变颜色的效果

将每个单元格与其中的信息对应起来

在这里可以发现每个单元格与其中的信息是一一对应的关系,我们就可以采用字典这种数据结构。

Dictionary<System.Windows.Shapes.Rectangle, string> rectangleText = new Dictionary<System.Windows.Shapes.Rectangle, string>();
 // 将单元格与对应的信息存入字典
 rectangleText[rectangle] = textBlock.Text;

这样就实现了每个单元格与其中信息的一一对应。

ListBox的使用

首先设计两个类。

public class SelectedRect
{      
    public string? Name {  get; set; }
}

表示选中的单元格,只有一个属性就是它所存储的信息。

public class SelectedRects : ObservableCollection<SelectedRect>
{

}

表示选中的多个单元格,继承自
ObservableCollection<SelectedRect>

ObservableCollection<T>
是.NET框架中的一个类,它表示一个动态数据集合,当添加、删除项或者刷新整个列表时,它会提供通知。这对于数据绑定非常有用,因为当集合改变时,UI可以自动更新以反映这些更改。

 SelectedRects selectedRects;
 public Drawing()
 {
     InitializeComponent();
     this.selectedRects = new SelectedRects();
     DataContext = selectedRects;

 }


WPF(Windows Presentation Foundation)
中,
DataContext
是一个非常重要的概念,它是数据绑定的基础。
DataContext
是定义在
FrameworkElement
类中的一个属性,几乎所有的WPF控件都继承自
FrameworkElement
,因此几乎所有的WPF控件都有
DataContext
属性。
DataContext
属性通常被设置为一个对象,这个对象包含了绑定到界面元素的数据。当你在XAML中创建数据绑定时,绑定表达式会查找
DataContext
中的属性。

需要注意的是,
DataContext
是可以继承的,如果一个元素的
DataContext
没有被显式设置,它将使用其父元素的
DataContext
。这使得你可以在窗口级别设置
DataContext
,然后在窗口的所有子元素中使用数据绑定。

在这里我们就是这样设置了窗口的
DataContext
属性为
selectedRects

现在我们修改点击事件处理程序:

 private void Rectangle_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
 {
     System.Windows.Shapes.Rectangle? rectangle = sender as System.Windows.Shapes.Rectangle;
     if (rectangle != null)
     {
         // 改变矩形的颜色以表示它被选中
         rectangle.Stroke = System.Windows.Media.Brushes.Red;
         
         string text = rectangleText[rectangle];
         
         SelectedRect selectedRect = new SelectedRect();
         selectedRect.Name = text;
         selectedRects.Add(selectedRect);
        
     }

 }

 private void Rectangle_MouseRightButtonDown(object sender, MouseButtonEventArgs e)
 {
     System.Windows.Shapes.Rectangle? rectangle = sender as System.Windows.Shapes.Rectangle;
     if (rectangle != null)
     {
         // 改变矩形的颜色以表示它被取消选中
         rectangle.Stroke = System.Windows.Media.Brushes.Blue;
         
         string text = rectangleText[rectangle];
         
         var selectedRect = selectedRects.Where(x => x.Name == text).FirstOrDefault();
         if (selectedRect != null)
         {
             selectedRects.Remove(selectedRect);
         }


     }

 }

在ListBox设置数据绑定:

 <ListBox Grid.Column="1" SelectedIndex="0" Margin="10,0,10,0"
           ItemsSource="{Binding}">
    
 </ListBox>

现在来看看效果:

image-20240419093037392

我们会发现在ListBox中只会显示类名,并不会显示类中的信息。

这是为什么呢?

因为我们只设置了数据绑定,ListBox知道它的数据来自哪里了,但是我们没有设置
数据模板
,ListBox不知道该按怎样的方式显示数据。

数据模板的使用

现在我们就来设置一下数据模板,先来介绍一下数据模板。


WPF(Windows Presentation Foundation)
中,
数据模板(DataTemplate)
是一种定义数据视觉表示的方式。它允许你自定义如何显示绑定到控件的数据。

数据模板非常强大,它可以包含任何类型的元素,并可以使用复杂的绑定和样式。通过使用数据模板,你可以创建丰富和个性化的UI,而无需在代码中手动创建和管理元素。

现在开始尝试去使用数据模板吧。

在xaml中添加:

<Window.Resources>
    <DataTemplate x:Key="MyTemplate">
        <TextBlock  Text="{Binding Path=Name}"/>
    </DataTemplate>
</Window.Resources>

<Window.Resources>
:这是一个资源字典,它包含了在整个窗口中都可以使用的资源。在这个例子中,它包含了一个数据模板。
<DataTemplate x:Key="MyTemplate">
:这定义了一个数据模板,并给它指定了一个键"MyTemplate"。这个键可以用来在其他地方引用这个模板。
<TextBlock Text="{Binding Path=Name}"/>
:这是数据模板的内容。它是一个TextBlock,其Text属性绑定到数据对象的Name属性。
{Binding Path=Name}
是一个绑定表达式,它告诉WPF查找数据对象中名为Name的属性,并将其值绑定到TextBlock的Text属性。


ListBox
使用这个数据模板:

<ListBox Grid.Column="1" SelectedIndex="0" Margin="10,0,10,0"
          ItemsSource="{Binding}"
         ItemTemplate="{StaticResource MyTemplate}">
   
</ListBox>

现在再来看一下效果:

显示效果

发现可以正常显示数据了,但是还有一个问题,就是会重复添加,最后解决这个问题就好了!

修改鼠标左键点击事件处理程序:

 // 鼠标事件处理器,左键点击表示选中
 private void Rectangle_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
 {
     System.Windows.Shapes.Rectangle? rectangle = sender as System.Windows.Shapes.Rectangle;
     if (rectangle != null)
     {
         // 改变矩形的颜色以表示它被选中
         rectangle.Stroke = System.Windows.Media.Brushes.Red;
         string text = rectangleText[rectangle];
         if (selectedRects.Where(x => x.Name == text).Any())
         {

         }
         else
         {
             SelectedRect selectedRect = new SelectedRect();
             selectedRect.Name = text;
             selectedRects.Add(selectedRect);
         }

     }

 }

现在再来看看最后的效果:

最后的效果

全部代码

xaml:

<Window x:Class=""
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local=""
        xmlns:hc="https://handyorg.github.io/handycontrol"
        mc:Ignorable="d"
        Title="Drawing" Height="450" Width="800">
    <Window.Resources>
        <DataTemplate x:Key="MyTemplate">
            <TextBlock  Text="{Binding Path=Name}"/>
        </DataTemplate>
    </Window.Resources>
    <StackPanel>
        <hc:Row Margin="0,20,0,0">
            <hc:Col Span="8">
                <Label Content="画矩形"></Label>
            </hc:Col>
            <hc:Col Span="8">
                <Button Style="{StaticResource ButtonPrimary}" Content="开始"
         Click="Button_Click_DrawRect"/>
            </hc:Col>
            <hc:Col Span="8">
                <Button Style="{StaticResource ButtonPrimary}" Content="清空"
                        Click="Button_Click_Clear"/>
            </hc:Col>
        </hc:Row>
        <Grid>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="*"/>
                <ColumnDefinition Width="*"/>
            </Grid.ColumnDefinitions>

            <Canvas Grid.Column="0" Background="Azure" x:Name="myCanvas1" Height="400">
                <!-- 在这里添加你的元素 -->
            </Canvas>


            <ListBox Grid.Column="1" SelectedIndex="0" Margin="10,0,10,0"
                      ItemsSource="{Binding}"
                     ItemTemplate="{StaticResource MyTemplate}">
               
            </ListBox>
        </Grid>



    </StackPanel>
</Window>

cs:

namespace xxx
{
    /// <summary>
    /// Drawing.xaml 的交互逻辑
    /// </summary>
    public partial class Drawing : System.Windows.Window
    {
        Dictionary<System.Windows.Shapes.Rectangle, string> rectangleText = new Dictionary<System.Windows.Shapes.Rectangle, string>();
        SelectedRects selectedRects;
        public Drawing()
        {
            InitializeComponent();
            this.selectedRects = new SelectedRects();
            DataContext = selectedRects;

        }

        private void Button_Click_DrawRect(object sender, RoutedEventArgs e)
        {
            int Row = 4;
            int Col = 3;
            
            for(int i = 0; i < Row; i++)
            {
                for(int j = 0; j< Col; j++) 
                {
                    // 添加矩形
                    System.Windows.Shapes.Rectangle rectangle = new System.Windows.Shapes.Rectangle
                    {
                        Width = 50,
                        Height = 50,
                        Stroke = System.Windows.Media.Brushes.Blue,

                        // 设置填充颜色为透明色
                        Fill = System.Windows.Media.Brushes.Transparent,
                        StrokeThickness = 1
                    };

                    // 添加鼠标事件处理器,左键点击表示选中
                    rectangle.MouseLeftButtonDown += Rectangle_MouseLeftButtonDown;

                    // 添加鼠标事件处理器,右键点击表示取消选中
                    rectangle.MouseRightButtonDown += Rectangle_MouseRightButtonDown;

                    Canvas.SetLeft(rectangle, 80 + 50 * j);
                    Canvas.SetTop(rectangle, 50 + 50 * i);
                  
                    myCanvas1.Children.Add(rectangle);

                    // 在矩形内部添加文字
                    TextBlock textBlock = new TextBlock
                    {
                        Text = i + "-" + j,
                        Foreground = System.Windows.Media.Brushes.Black,
                        FontSize = 12
                    };

                    Canvas.SetLeft(textBlock, 80 + 50 * j + 10);
                    Canvas.SetTop(textBlock, 50 + 50 * i + 10);

                    myCanvas1.Children.Add(textBlock);

                    // 将单元格与对应的信息存入字典
                    rectangleText[rectangle] = textBlock.Text;
                }
               
              
            }
          

        }

        private void Button_Click_Clear(object sender, RoutedEventArgs e)
        {
            myCanvas1.Children.Clear();
        }

        // 鼠标事件处理器,左键点击表示选中
        private void Rectangle_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
        {
            System.Windows.Shapes.Rectangle? rectangle = sender as System.Windows.Shapes.Rectangle;
            if (rectangle != null)
            {
                // 改变矩形的颜色以表示它被选中
                rectangle.Stroke = System.Windows.Media.Brushes.Red;
                string text = rectangleText[rectangle];
                if (selectedRects.Where(x => x.Name == text).Any())
                {

                }
                else
                {
                    SelectedRect selectedRect = new SelectedRect();
                    selectedRect.Name = text;
                    selectedRects.Add(selectedRect);
                }

            }

        }

        // 鼠标事件处理器,右键点击表示选中
        private void Rectangle_MouseRightButtonDown(object sender, MouseButtonEventArgs e)
        {
            System.Windows.Shapes.Rectangle? rectangle = sender as System.Windows.Shapes.Rectangle;
            if (rectangle != null)
            {
                // 改变矩形的颜色以表示它被取消选中
                rectangle.Stroke = System.Windows.Media.Brushes.Blue;
                string text = rectangleText[rectangle];
                var selectedRect = selectedRects.Where(x => x.Name == text).FirstOrDefault();
                if (selectedRect != null)
                {
                    selectedRects.Remove(selectedRect);
                }


            }

        }
    }
}

总结

本文通过一个小示例,跟大家介绍了如何在WPF上绘制矩形,并在其中添加文本,同时也介绍了ListBox的使用,通过数据绑定与数据模板显示我们选中的单元格内的文本信息。希望对与我一样正在学习WPF或者对WPF感兴趣的同学有所帮助。

接下来我将持续更新“深度解读《深度探索C++对象模型》”系列,敬请期待,欢迎关注!也可以关注公众号:iShare爱分享,自动获得推文和全部的文章列表。

没有启用返回值优化时,怎么从函数内部返回对象

当在函数的内部中返回一个局部的类对象时,是怎么返回对象的值的?请看下面的代码片段:

class Object {}

Object foo() {
    Object b;
    // ...
	return b;
}

Object a = foo();

对于上面的代码,是否一定会从foo函数中拷贝对象到对象a中,如果Object类中定义了拷贝构造函数的话,拷贝构造函数是否一定会被调用?答案是要看Object类的定义和编译器的实现策略有关。我们细化一下代码来进一步分析具体的表现行为,请看下面的代码:

#include <cstdio>

class Object {
public:
    Object() {
        printf("Default constructor\n");
        a = b = c = d = 0;
    }
    int a;
    int b;
    int c;
    int d;
};

Object foo() {
    Object p;
    p.a = 1;
    p.b = 2;
    p.c = 3;
    p.d = 4;
    return p;
}

int main() {
    Object obj = foo();
    printf("%d, %d, %d, %d\n", obj.a, obj.b, obj.c, obj.d);

    return 0;
}

编译成对应的汇编代码,看一下是怎么从foo函数中返回一个对象的,下面节选main和foo函数的汇编代码:

foo():														# @foo()
    push    rbp
    mov     rbp, rsp
    sub     rsp, 16
    lea     rdi, [rbp - 16]
    call    Object::Object() [base object constructor]
    mov     dword ptr [rbp - 16], 1
    mov     dword ptr [rbp - 12], 2
    mov     dword ptr [rbp - 8], 3
    mov     dword ptr [rbp - 4], 4
    mov     rax, qword ptr [rbp - 16]
    mov     rdx, qword ptr [rbp - 8]
    add     rsp, 16
    pop     rbp
    ret
main:															# @main
    push    rbp
    mov     rbp, rsp
    sub     rsp, 32
    mov     dword ptr [rbp - 4], 0
    call    foo()
    mov     qword ptr [rbp - 24], rax
    mov     qword ptr [rbp - 16], rdx
    mov     esi, dword ptr [rbp - 24]
    mov     edx, dword ptr [rbp - 20]
    mov     ecx, dword ptr [rbp - 16]
    mov     r8d, dword ptr [rbp - 12]
    lea     rdi, [rip + .L.str]
    mov     al, 0
    call    printf@PLT
    xor     eax, eax
    add     rsp, 32
    pop     rbp
    ret

从汇编代码中看到,在foo函数内部构造了一个Object类的对象(第5、6行),然后对它的成员进行赋值(第7行到第10行),最后通过将对象的值拷贝到rax和rdx寄存器中作为返回值返回(第11、12行)。在main函数中的第22、23代码,将返回值从rax和rdx寄存器中拷贝到栈空间中,这里没有构造对象,直接采用拷贝的方式拷贝内容,可见在这种情况下编译器是直接拷贝对象内容的方式来返回一个局部对象的。

启用返回值优化的条件和编译器的实现分析

如果Object类中有定义了一个拷贝构造函数,在这种情况下表现行为又是怎样的?在上面从C++代码中加入拷贝构造函数:

Object(const Object& rhs) {
    printf("Copy constructor\n");
    memcpy(this, &rhs, sizeof(Object));
}

编译运行,输出结果如下:

Default constructor
1, 2, 3, 4

神奇的是拷贝构造函数被没有如预期地被调用,甚至查看汇编代码都没有生成拷贝构造函数的代码(因为没有调用,编译器优化掉了)。我们再来看看foo和main函数的汇编代码,看看和上面的汇编代码有什么区别。

foo():                           # @foo()
    push    rbp
    mov     rbp, rsp
    sub     rsp, 32
    mov     qword ptr [rbp - 24], rdi       # 8-byte Spill
    mov     rax, rdi
    mov     qword ptr [rbp - 16], rax       # 8-byte Spill
    mov     qword ptr [rbp - 8], rdi
    call    Object::Object() [base object constructor]
    mov     rdi, qword ptr [rbp - 24]       # 8-byte Reload
    mov     rax, qword ptr [rbp - 16]       # 8-byte Reload
    mov     dword ptr [rdi], 1
    mov     dword ptr [rdi + 4], 2
    mov     dword ptr [rdi + 8], 3
    mov     dword ptr [rdi + 12], 4
    add     rsp, 32
    pop     rbp
    ret
main:															# @main
    push    rbp
    mov     rbp, rsp
    sub     rsp, 32
    mov     dword ptr [rbp - 4], 0
    lea     rdi, [rbp - 24]
    call    foo()
    mov     esi, dword ptr [rbp - 24]
    mov     edx, dword ptr [rbp - 20]
    mov     ecx, dword ptr [rbp - 16]
    mov     r8d, dword ptr [rbp - 12]
    lea     rdi, [rip + .L.str]
    mov     al, 0
    call    printf@PLT
    xor     eax, eax
    add     rsp, 32
    pop     rbp
    ret

从汇编代码中看到,foo函数内部中不再构造一个局部对象然后初始化后再将这个对象拷贝返回,而是传递了一个对象的地址给foo函数(第24、25行),foo函数对传递过来的这个对象进行构造(第5到第9行),然后对对象的成员进行赋值(第12到15行),foo函数结束之后,在main函数中就可以直接使用这个被构造和赋值后的对象了,第26到29行就是取各成员的值然后调用printf函数打印出来。也就是说原先的代码被编译器改写了,如下面的伪代码所示:

Object obj = foo();
// 将被改成:
Object obj;	// 这里不需要调用默认构造函数
foo(obj);

// 相应地foo函数将被改写定义:
void foo(Object& obj) {
    obj.Object::Object();	// 调用Object的默认构造函数
    obj.a = 1;
    obj.b = 2;
    obj.c = 3;
    obj.d = 4;
    return;
}

看起来像是拷贝构造函数的加入激活了编译器NRV(Named Return Value)优化,为什么有拷贝构造函数的存在就会触发NRV优化呢?原因就是既然程序中定义了拷贝构造函数,根据我们之前的分析,说明是要处理拷贝大块的内存空间等之类的操作,不仅仅是普通的数据成员的拷贝,如果只是拷贝数据成员可以不必定义拷贝构造函数,编译器会采用更高效的逐成员拷贝的方法,编译器内部就可以帮程序员做好了,
所以有拷贝构造函数的存在就说明有需要低效的拷贝动作,那么就要想办法消除掉拷贝的操作,那么启用NRV优化就是一项提高效率的做法了。

那么是不是只有存在拷贝构造函数编译器才会启用NRV优化呢?我们继续来修改代码,类中加入一个大数组,同时把拷贝构造函数去掉:

class Object {
public:
    Object() {
        printf("Default constructor\n");
        a = b = c = d = 0;
    }
    int a;
    int b;
    int c;
    int d;
    int buf[100];
};

这样修改之后的汇编代码跟之前的基本一样(汇编代码跟上面基本一样就没贴了),有区别的地方就是对象占用的内存空间变大了,这说明没有定义拷贝构造函数的情况下编译器也有可能启用了NRV优化,在对象占用的内存空间较大的时候,这时不再适合使用寄存器来传送对象的内容了,如果采用栈空间来返回结果的话,会涉及到内存的拷贝,效率较低,所以启用NRV优化则有效率上的提升。

启用返回值优化后的效率提升

那么启用NRV优化与不启用优化,两者之间的效率对比究竟差了多少?我们还是以上面的例子来测试,默认情况下编译器是开启了这个优化的,如果想要禁用这个优化,可以在编译时加入
-fno-elide-constructors
选项关闭它。为了不影响效率,把打印都去掉,在main函数中加入时间计时,下面是完整的代码:

#include <cstdio>
#include<chrono>
using namespace std::chrono;

class Object {
public:
    Object() {}
    int a;
    int b;
    int c;
    int d;
    int buf[100];
};

Object foo(int i) {
    Object p;
    p.a = 1;
    p.b = 2;
    p.c = 3;
    p.d = 4;
    p.buf[0] = i;
    p.buf[99] = i;
    return p;
}

int main() {
    auto start = system_clock::now();
    for (auto i = 0; i < 10000000; ++i) {
        Object a = foo(i);
    }
    auto end = system_clock::now();
    auto duration = duration_cast<milliseconds>(end-start);
    printf("spend %lldms\n", duration.count());

    return 0;
}

下面是在我的Apple M1机器上的测试结果,每种情况都是取测试10次然后取平均值。

启用NRV优化 未启用NRV优化
56.3ms 186.7

未优化的时间多花了130.4ms,时间上是启用优化后的时间的3倍多。

返回值优化的缺点

从测试结果来看,NRV优化看起来很美好,那么NRV优化是否一切都完美无缺呢?其实NRV优化也存在一些不足或者说不尽如人意的地方:

  • 是否开启了NRV优化的问题,NRV优化并不是C++标准中规定的东西,各家编译器的实现未必一定支持它,或者说启用它的条件和规则也不尽相同,例如clang或者g++,像我上面提到的那两种情况下就会开启优化,微软的Visual Studio编译器则默认不会启用,需要设置优化选项之后才会启用。所以写一些跨平台的代码的时候需要注意一下,做到心中有数。
  • 未能启用NRV优化的情况,NRV优化并非在所有的情况下、所有的代码中都能够启用,可能在某些条件限制下编译器不能够启用优化,比如代码逻辑太复杂的情况下。
  • 优化不是预期的需求,优化可能在无声无息中完成了,但是却有可能不是你想要的结果,比如你期待在拷贝构造函数中做一些事情,然后在析构函数中做相反的一些事情,但是拷贝构造函数并未如预期中的被调用了,导致了程序运行的错误。

总之,需要做到对编译器背后的行为有深入的理解,就能做到心中有数,写出既高效又安全的代码。


如果您感兴趣这方面的内容,请在微信上搜索公众号iShare爱分享并关注,以便在内容更新时直接向您推送。

本文介绍在
EndNote
软件中,使得
参考文献
按照
语种
排列,
中文在前

英文在后
的方法。

前期我们在
EndNote参考文献格式Output Styles界面介绍
一文中,详细介绍了文献管理软件
EndNote
的引用格式自定义方法,其中我们设置了
将参考文献部分的文章按照文章语种进行排序
,而这一设置在默认情况下是
英文在前中文在后

而目前,需要对一篇论文的参考文献部分原本默认的
中、英文引文排列顺序
加以更进一步的修改。其中,原本的参考文献是
英文文章在前,中文文章在后
状态,如下图;现在需要修改为
中文在前,英文在后
的状态,如下下图。因此,我们就在上述博客的基础之上,进行进一步的格式修改。

image

而下图,则是修改后的状态。

image

话不多说,我们开始修改。在这里需要注意,请先按照本文开头提及的
EndNote参考文献格式Output Styles界面介绍
的方法,将
论文语种

Language
)作为参考文献的第一排序指标(如下图即可);这一步骤的具体操作方法在本文就不赘述~

首先,打开
EndNote
软件,新建一个“
Group Set
”。

并对其进行重命名,名字大家只要自己今后可以辨认就好。

随后,在新建的“
Group Set
”中再新建两个“
Group
”,一个用来存放中文文献,一个存放英文。

同样的,大家对两个“
Group
”重命名一下。

随后,在
EndNote
左侧,找到我们需要修改参考文献顺序的
Word
文件(如果没出现这个文件的话,大家可以直接在对应的
Word
文件中随便用
EndNote
引用一篇文章),然后将其按照“
Title
”排序。

随后,利用“
Shift
”与左键结合方式,选中全部英文文献。

将其拖入到刚刚建立的英文文献分组中。

随后,用同样方法将中文文献拖入到中文分组中。

随后,选中新建的中文分组,选择“
Tools
”→“
Change/Move/Copy Fields..
.”选项。

按照如下的方式,进行修改与确认。这一步骤是将全部的中文文献对应的“
Language
”字段修改为
Chinese

确认后会有一个弹窗,点击确定即可。

随后可以看到设置完成。

对英文文献进行同样的操作,此时修改为
English

随后,到
Word
文件中,更新引文与参考文献。

可以看到,参考文献部分已经是中文在前、英文在后了。

至此,大功告成。

前面我们提到:OS希望在保持控制权的同时,为用户提供高性能的并发。

那么OS究竟是如何保持对计算机的控制权呢?这似乎是一个令人迷惑(但很重要!)的问题:OS也是进程,自然也需要计算资源。那既然我们希望一直保持OS的控制权,难道OS需要一直占用计算资源么?当然不是这样。

在微内核OS中,一般有两种模式 —— 内核模式(Kernel Mode)和 用户模式(User Mode),普通用户进程基本都运行在用户模式中,只能执行一些一般的、基本的操作。那不一般的操作(我们称之为“特权操作”)有哪些呢?比如I/O操作、中断处理、时钟管理、还有相当重要的进程调度操作等,这些“特权操作”只能在内核模式中由操作系统进行,而用户模式下则不能执行任何特权操作。

上述内核模式与用户模式的关系类似于什么呢?其实有点像父母给小孩子的安全屋:所有比较危险的东西(比如带电的插座、窗户等)都被保护起来了,小孩子可以在里边安全的活动。但是一旦他想做任何“危险行为”,就应该找他的父母来完成那些“危险行为”。

类似的,一旦某个程序希望执行某个特权操作,它就会调用OS内核(syscall)来执行,这样CPU的控制权就被原先的程序移交给了OS。

好,看起来我们已经保证了OS的控制权:通过用户进程主动syscall移交控制权。但是这个方式没有解决另外一个问题:如果一个进程一直不愿意交出CPU控制权怎么办?这样的进程很容易实现:一个while(true);无限循环就行。对于这些不友好的进程,我们该如何处理才能保证OS的控制权?

(想一想,你可以想到的!)

没错,我们可以定时将控制权移交给OS,这可以利用时钟(如石英振荡器)来实现,每隔一段时间定时syscall,强制将计算机控制权移交给OS就行,当然这需要硬件中断的支持。

综上,现在我们有两种方式来保证OS对计算机的控制权:1 友好的进程会主动syscall来移交控制权;2 对于不友好的进程,我们使用定时中断的方式获得控制权。

当然,上述两种方式还带来了2个小问题:

1.    所有用户模式的进程在将控制权移交给OS后(或主动或被动),它都相当于从原本的运行状态被硬生生地打断了。那么它下一次运行的时候,我们是不是需要将计算机恢复到它被打断之前的状态,从而接着运行呢?没错,当然是需要的,我们可以用Process Control Block来实现,这个过程就叫
Context Switch —— 上下文切换

2.    OS或主动或被动地获得控制权后,都必须要决定接下来要运行哪个用户进程,那么OS是如何来决定接下来运行什么进程的呢?这就要通过
调度算法 —— Scheduler
来实现。