深入剖析 linux GCC 4.4 的 STL string

转自:

深入剖析 linux GCC 4.4 的 STL string

本文通过研究STL源码来剖析C++中标准模板块库std::string运行机理,重点研究了其中的引用计数和Copy-On-Write技术。

平台:x86_64-redhat-linux
gcc version 4.4.6 20110731 (Red Hat 4.4.6-3) (GCC)

1. 问题提出

最近在我们的项目当中,出现了两次与使用string相关的问题。

1.1. 问题1:新代码引入的Bug

前一段时间有一个老项目来一个新需求,我们新增了一些代码逻辑来处理这个新需求。测试阶段没有问题,但上线之后,偶尔会引起错误的逻辑输出甚至崩溃。这个问题困扰着我们很久。我们对新增代码做周详单元测试和集成测试都没有发现问题,最后只能逼迫我们去看那一大段未修改过原始代码逻辑。该项目中经常会碰到使用string,原始代码中有这样一段逻辑引起了我们的怀疑:

1string string_info;
2//... 对string_info的赋值操作
3char* p = (char*)string_info.data();

在严格的检查下和逻辑判断后,某些逻辑分支会对p指向的内容进行一些修改。这样虽然危险,但一直工作正常。联想到我们最近的修改:将string_info这个string对象拷贝了一份,然后进行一些处理。我们意识到string的Copy-On-Write和引用计数技术可能会导致我们拷贝的这个string并没有真正的实现数据拷贝。在做了一些测试和研究之后,我们确信了这一点。如是对上述代码进行了修正处理如下:

1char* p = &(string_info[0]);

然后对项目类似的地方都做了这样的处理之后,测试,上线,一切OK,太完美了。

1.2. 问题2:性能优化

最近做一个项目的重构,对相关代码进行性能分析profile时发现memcpy的CPU占比比较高,达到8.7%,仔细检查代码中,发现现有代码大量的map查找操作。map定义如下:

1typedef std::map ssmap;
2ssmap info_map;

查找的操作如下:

1info_map["some_key"] = some_value;

我们不经意间就会写出上述代码,如果改为下述代码,性能会好很多:

1static const std::string __s_some_key   = "some_key";
2info_map[__s_some_key] = some_value;

这是因为第一种代码,每次查找都构造一个临时的string对象,同时会将“some_key”这个字符串拷贝一份。修改之后的代码,只需要在第一次初始化时候构造一次,以后每次调用都不会进行拷贝,因此效率上要好很多。类似代码都经过这样优化之后,memcpy的CPU占比下来了,降到4.3%。

下面我们通过深入string的源码内部来解释上述两个问题的解决过程和思路。

2. std::string定义

STL中的字符串类string的定义如下:

1template<typename _CharT, typename _Traits , typename _Alloc> class basic_string;
2typedef basic_string <char, char_traits<char >, allocator< char> > string;

不难发现string在栈内存空间上只占用一个指针(_CharT* _M_p)的大小空间,因此sizeof(string)==8。其他信息都存储在堆内存空间上。

问题1:
我们有下面这一条C++语句:

1string name;

请问,name这个变量总共带来多大的内存开销?这个问题我们稍后解答。

3. std::string内存空间布局

下面我们通过常见的用法来剖析一下string对象内部内存空间布局情况。
最常见的string用法是通过c风格字符串构造一个string对象,例如:
string name(“zieckey”);

其调用的构造函数定义如下:

1basic_string(const _CharT* __s, const _Alloc& __a)
2: _M_dataplus( _S_construct(__s , __s ? __s + traits_type ::length( __s) :
3              __s + npos , __a), __a)
4{}

 

该构造函数直接调用 _S_construct 来构造这个对象,定义如下:

01template<typename _CharT, typename _Traits , typename _Alloc>
02template<typename _InIterator>
03_CharT*
04basic_string<_CharT , _Traits, _Alloc>::
05_S_construct(_InIterator __beg, _InIterator __end , const _Alloc& __a ,
06             input_iterator_tag)
07{
08    // Avoid reallocation for common case.
09    _CharT __buf[128];
10    size_type __len = 0;
11    while ( __beg != __end && __len < sizeof(__buf ) / sizeof( _CharT))
12    {
13        __buf[__len ++] = *__beg;
14        ++ __beg;
15    }
16 
17    //构造一个 _Rep 结构体,同时分配足够的空间,具体见下面内存映像图示
18    _Rep* __r = _Rep ::_S_create( __len, size_type (0), __a);
19 
20    //拷贝数据到 string对象内部
21    _M_copy( __r->_M_refdata (), __buf, __len);
22    __try
23    {
24        while (__beg != __end)
25        {
26            if (__len == __r-> _M_capacity)
27            {
28                // Allocate more space.
29                _Rep* __another = _Rep:: _S_create(__len + 1, __len, __a);
30                _M_copy(__another ->_M_refdata(), __r->_M_refdata (), __len);
31                __r->_M_destroy (__a);
32                __r = __another ;
33            }
34            __r->_M_refdata ()[__len++] = * __beg;
35            ++ __beg;
36        }
37    }
38    __catch(...)
39    {
40        __r->_M_destroy (__a);
41        __throw_exception_again;
42    }
43    //设置字符串长度、引用计数以及赋值最后一个字节为结尾符 char_type()
44    __r-> _M_set_length_and_sharable(__len );
45 
46    //最后,返回字符串第一个字符的地址
47    return __r->_M_refdata ();
48}
49 
50template<typename _CharT, typename _Traits , typename _Alloc>
51typename basic_string <_CharT, _Traits, _Alloc >::_Rep*
52basic_string<_CharT , _Traits, _Alloc>::_Rep ::
53_S_create(size_type __capacity, size_type __old_capacity ,
54          const _Alloc & __alloc)
55{
56    // 需要分配的空间包括:
57    //  一个数组 char_type[__capacity]
58    //  一个额外的结尾符 char_type()
59    //  一个足以容纳 struct _Rep 空间
60    // Whew. Seemingly so needy, yet so elemental.
61    size_type __size = (__capacity + 1) * sizeof( _CharT) + sizeof (_Rep);
62 
63    void* __place = _Raw_bytes_alloc (__alloc). allocate(__size ); //申请空间
64 
65    _Rep * __p = new (__place) _Rep;// 在地址__place 空间上直接 new对象( 称为placement new)
66    __p-> _M_capacity = __capacity ;
67    __p-> _M_set_sharable();// 设置引用计数为0,标明该对象只为自己所有
68    return __p;
69}

 

_Rep定义如下:

1struct _Rep_base
2{
3    size_type               _M_length;
4    size_type               _M_capacity;
5    _Atomic_word            _M_refcount;
6};

 

至此,我们可以回答上面“问题1”中提出的问题:
上文中”string name;”这个name对象所占用的总空间为33个字节,具体如下:

1sizeof(std::string) + 0 + sizeof('') + sizeof(std::string::_Rep)

 

其中:sizeof(std::string)为栈空间

上文中的提到的另一条C++语句 string name(“zieckey”); 定义了一个string变量name,其内存空间布局如下:

 

4. 深入string内部源码

4.1. string copy与strncpy

长期以来,经常看到有人对std::string赋值拷贝与strncpy之间的效率进行比较和讨论。下面我们通过测试用例来进行一个基本的测试:

01#include<iostream>
02#include<cstdlib>
03#include<string>
04#include<ctime>
05#include<cstring>
06 
07using namespace std;
08 
09const int array_size = 200;
10const int loop_count = 1000000;
11 
12void test_strncpy ()
13{
14    char s1[array_size ];
15    char* s2= new char[ array_size];
16    memset( s2, 'c' , array_size);
17    size_t start=clock ();
18    forint i =0;i!= loop_count;++i ) strncpy( s1,s2 , array_size);
19    cout<< __func__ << " : " << clock()- start<<endl ;
20    delete s2;
21    s2 = NULL;
22}
23 
24void test_string_copy ()
25{
26    string s1;
27    string s2;
28    s2. append(array_size , 'c');
29    size_t start=clock ();
30    forint i =0;i!= loop_count;++i ) s1= s2;
31    cout<< __func__ << " : " << clock()- start<<endl ;
32}
33 
34int main ()
35{
36    test_strncpy();
37    test_string_copy();
38    return 0;
39}

 

使用g++ -O3编译,运行时间如下:

test_strncpy : 40000
test_string_copy : 10000

字符串strncpy的运行时间居然是string copy的4倍。究其原因就是因为,string copy是基于引用计数技术,每次copy的代价非常小。
测试中我们还发现,如果array_size在10个字节以内的话,两者相差不大,随着array_size的变大,两者的差距也越来越大。例如,在array_size=1000的时候,strncpy就要慢13倍。

4.2. 通过GDB调试查看引用计数变化

上面的测试结论非常好,打消了大家对string性能问题的担忧。下面我们通过一段程序来验证引用计数在这一过程中的变化和作用。
请先看一段测试代码:

01#include <assert.h>
02#include <iostream>
03#include <string>
04 
05using namespace std;
06 
07int main ()
08{
09    string a = "0123456789abcdef" ;
10    string b = a ;
11    cout << "a.data() =" << (void *)a. data() << endl ;
12    cout << "b.data() =" << (void *)b. data() << endl ;
13    assert( a.data () == b. data());
14    cout << endl;
15 
16    string c = a ;
17    cout << "a.data() =" << (void *)a. data() << endl ;
18    cout << "b.data() =" << (void *)b. data() << endl ;
19    cout << "c.data() =" << (void *)c. data() << endl ;
20    assert( a.data () == c. data());
21 
22    cout << endl;
23    c[0] = '1';
24    cout << "after write:\n";
25    cout << "a.data() =" << (void *)a. data() << endl ;
26    cout << "b.data() =" << (void *)b. data() << endl ;
27    cout << "c.data() =" << (void *)c. data() << endl ;
28    assert( a.data () != c. data() && a .data() == b.data ());
29    return 0;
30}

 

运行之后,输出:

a.data() =0xc22028
b.data() =0xc22028

a.data() =0xc22028
b.data() =0xc22028
c.data() =0xc22028

after write:
a.data() =0xc22028
b.data() =0xc22028
c.data() =0xc22068

上述代码运行的结果输出反应出,在我们对b、c赋值之后,a、b、c三个string对象的内部数据的内存地址都是一样的。只有当我们对c对象进行修改之后,c对象的内部数据的内存地址才不一样,这一点是是如何做到的呢?

我们通过gdb调试来验证引用计数在上述代码执行过程中的变化:

01(gdb) b 10
02Breakpoint 1 at 0x400c35: file string_copy1.cc, line 10.
03(gdb) b 16
04Breakpoint 2 at 0x400d24: file string_copy1.cc, line 16.
05(gdb) b 23
06Breakpoint 3 at 0x400e55: file string_copy1.cc, line 23.
07(gdb) r
08Starting program: [...]/unixstudycode/string_copy/string_copy1
09[Thread debugging using libthread_db enabled]
10 
11Breakpoint 1, main () at string_copy1.cc:10
1210          string b = a;
13 
14(gdb) x/16uba._M_dataplus._M_p-8      
150x602020:       0       0       0       0       0       0       0       0
160x602028:       48      49      50      51      52      53      54      55

此时对象a的引用计数是0

1(gdb) n                                
211          cout &lt;&lt; "a.data() =" &lt;&lt; (void*)a.data() &lt;&lt; endl;

b=a 将a赋值给b,string copy

1(gdb) x/16ub a._M_dataplus._M_p-8
20x602020:       1       0       0       0       0       0       0       0
30x602028:       48      49      50      51      52      53      54      55

此时对象a的引用计数变为1,表明有另一个对象共享该对象a

01(gdb) c
02Continuing.
03a.data() =0x602028
04b.data() =0x602028
05 
06Breakpoint 2, main () at string_copy1.cc:16
0716          string c = a;
08(gdb) x/16ub a._M_dataplus._M_p-8
090x602020:       1       0       0       0       0       0       0       0
100x602028:       48      49      50      51      52      53      54      55
11(gdb) n
1217          cout &lt;&lt; "a.data() =" &lt;&lt; (void*)a.data() &lt;&lt; endl;

c=a 将a赋值给c,string copy

1(gdb) x/16ub a._M_dataplus._M_p-8
20x602020:       2       0       0       0       0       0       0       0
30x602028:       48      49      50      51      52      53      54      55

此时对象a的引用计数变为2,表明有另外2个对象共享该对象a

01(gdb) c
02Continuing.
03a.data() =0x602028
04b.data() =0x602028
05c.data() =0x602028
06 
07Breakpoint 3, main () at string_copy1.cc:23
0823          c[0] = '1';
09(gdb) n
1024          cout &lt;&lt; "after write:\n";

对c的值进行修改

1(gdb) x/16ub a._M_dataplus._M_p-8
20x602020:       1       0       0       0       0       0       0       0
30x602028:       48      49      50      51      52      53      54      55

此时对象a的引用计数变为1

1(gdb) p a._M_dataplus._M_p      
2$3 = 0x602028 "0123456789abcdef"
3(gdb) p b._M_dataplus._M_p
4$4 = 0x602028 "0123456789abcdef"
5(gdb) p c._M_dataplus._M_p
6$5 = 0x602068 "1123456789abcdef"

此时对象c的内部数据内存地址已经与a、b不同了,即Copy-On-Write

上述GDB调试过程,清晰的验证了3个string对象a b c的通过引用计数技术联系在一起。

4.3. 源码分析string copy

下面我们阅读源码来分析。上述过程。
先看string copy过程的源码:

01//拷贝构造函数
02basic_string(const basic_string& __str)
03: _M_dataplus( __str._M_rep ()->_M_grab( _Alloc(__str .get_allocator()),
04              __str.get_allocator ()),
05              __str.get_allocator ())
06{}
07 
08_CharT* _M_grab(const _Alloc& __alloc1, const _Alloc& __alloc2)
09{
10    return (! _M_is_leaked() && __alloc1 == __alloc2)
11        ? _M_refcopy() : _M_clone (__alloc1);
12}
13 
14_CharT*_M_refcopy() throw ()
15{
16#ifndef _GLIBCXX_FULLY_DYNAMIC_STRING
17    if ( __builtin_expect(this != &_S_empty_rep(), false))
18#endif
19        __gnu_cxx::__atomic_add_dispatch (&this-> _M_refcount, 1);
20    return _M_refdata();
21}

 

上面几段源代码比较好理解,先后调用了basic_string (const basic_string& __str )拷贝构造函数、_M_grab、_M_refcopy,
_M_refcopy实际上就是调用原子操作__atomic_add_dispatch (确保线程安全)将引用计数+1,然后返回原对象的数据地址。
由此可以看到,string对象之间的拷贝/赋值代价非常非常小。

几个赋值语句之后,a、b、c对象的内存空间布局如下图所示:

 

4.4. Copy-On-Write

下面再来看”c[0] = ‘1’; “做了些什么:

01reference operator []( size_type __pos )
02{
03    _M_leak();
04    return _M_data ()[__pos ];
05}
06 
07void _M_leak ()    // for use in begin() & non-const op[]
08{
09    //前面看到 c 对象在此时实际上与a对象的数据实际上指向同一块内存区域
10    //因此会调用 _M_leak_hard()
11    if (! _M_rep ()->_M_is_leaked ())
12        _M_leak_hard ();
13}
14 
15void _M_leak_hard ()
16{
17    if ( _M_rep ()->_M_is_shared ())
18        _M_mutate (0, 0, 0);
19    _M_rep()-> _M_set_leaked ();
20}
21 
22void _M_mutate ( size_type __pos , size_type __len1, size_type __len2 )
23{
24    const size_type __old_size = this-> size ();//16
25    const size_type __new_size = __old_size + __len2 - __len1 ; //16
26    const size_type __how_much = __old_size - __pos - __len1 ; //16
27 
28    if ( __new_size > this -> capacity() || _M_rep ()->_M_is_shared ())
29    {
30        // 重新构造一个对象
31        const allocator_type __a = get_allocator ();
32        _Rep * __r = _Rep:: _S_create (__new_size , this-> capacity (), __a );
33 
34        // 然后拷贝数据
35        if (__pos )
36            _M_copy (__r -> _M_refdata(), _M_data (), __pos );
37        if (__how_much )
38            _M_copy (__r -> _M_refdata() + __pos + __len2 ,
39            _M_data () + __pos + __len1, __how_much );
40 
41        //将原对象上的引用计数减
42        _M_rep ()->_M_dispose ( __a);
43 
44        //绑定到新的对象上
45        _M_data (__r -> _M_refdata());
46    }
47    else if (__how_much && __len1 != __len2 )
48    {
49        // Work in-place.
50        _M_move (_M_data () + __pos + __len2 ,
51            _M_data () + __pos + __len1, __how_much );
52    }
53 
54    //最后设置新对象的长度和引用计数值
55    _M_rep()-> _M_set_length_and_sharable (__new_size );
56}

 

上面源码稍微复杂点,对c进行修改的过程分为以下两步:

  1. 第一步是判断是否为共享对象,(引用计数大于0),如果是共享对象,就拷贝一份新的数据,同时将老数据的引用计数值减1。
  2. 第二步:在新的地址空间上进行修改,从而避免了对其他对象的数据污染

由此可以看出,如果不是通过string提供的接口对string对象强制修改的话,会带来潜在的不安全性和破坏性。例如:

1char* p = const_cast<char*>(s1.data());
2p[0] = 'a';

上述代码对c修改(“c[0] = ‘1’; “)之后,a b c对象的内存空间布局如下:

Copy-On-Write的好处通过上文的解析是显而易见是,但也带来一些副作用。例如上述代码片段”c[0] = ‘1’; “如果是通过外部的强制操作可能会带来意想不到的结果。请看下面代码:

1char* pc = const_cast(c.c_str());
2pc[0] = '1';

这段代码通过强制修改c对象内部数据的值,看似效率上比operator[] 高,但同时也修改a、b对象的值,而这可能不是我们所希望看到的。这是我们需要提高警惕的地方。

5.   不宜使用string的例子 

我们项目组内部有一个分布式的内存kv系统,一般是md5做key,value是任意二进制数。当初设计的时候,考虑到内存容量始终有限,没有选择使用string,而是单独开发的key结构和value结构。下面是我们设计的key结构定义:

1struct Key
2{
3    uint64_t low;
4    uint64_t high;
5};

该结构所需内存大小为16字节,保持二进制的16字节MD5。相对于string做key来说,要节省33(参考上文string内存空间布局)个字节。例如,现在我们某个项目正在使用该系统的搭建的一个分布式集群,总共有100亿条记录,每条记录都节省33字节,总共节省内存空间:33*100亿=330G。由此可见,仅仅对key的一个小小改进,就能节省如此大的内存,还是非常值得。

6. 对比微软Visual Studio提供的STL版本

vc6.0的string实现是基于引用计数的,但不是线程安全的。但在后续版本的vc中去掉了引用计数技术,string copy 都直接进行深度内存拷贝。
由于string实现上的细节不一致,导致跨平台程序的移植带来潜在的风险。这种场合下,我们需要额外注意。

 

7. 总结

    1. 即使是一个空string对象,其所占内存空间也达到33字节,因此在内存使用要求比较严格的应用场景,例如memcached等,请慎重考虑使用string。
    2. string由于使用引用计数和Copy-On-Write技术,相对于strcpy,string copy的性能提升非常显著。
    3. 使用引用计数后,多个string指向同一块内存区域,因此,如果强制修改一个string的内容,会影响其他string。

转载于:https://www.cnblogs.com/lit10050528/p/4325979.html

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

java scanner以回车结束_请问Scanner在输入回车后,如何才能退出?

package com.imooc;import java.util.Scanner;/*kernal* 功能描述&#xff1a;* 为指定成绩加分&#xff0c;直到分数大于等于 60 为止,* 输出加分前和加分后的成绩&#xff0c;并统计加分的次数.*/public class addScore {public static void main(String[] args) {System.out…

HTML列表标记符有哪些,在HTML中,表格标记符为( )

3. (2017高一上东台月考) 阅读下面一段资料&#xff0c;判断在给出的几种说法中不正确的是( )资料&#xff1a;IP电话与传统电话IP电话是按国际互联网协议规定的网络技术内容开通的电话业务&#xff0c;中文翻译为网络电话或互联网电话&#xff0c;它是利用国际互联网Inetrne…

Linq 实现左连接,右连接

左连接&#xff1a; var LeftJoin from emp in ListOfEmployees join dept in ListOfDepartment on emp.DeptID equals dept.ID into JoinedEmpDept from dept in JoinedEmpDept.DefaultIfEmpty() select new { EmployeeName emp.Name, DepartmentName dept ! null ? de…

python怎样执行curl命令_python curl命令

今天下午来到&#xff0c;运行了curl -i url:port &#xff0c;然后返回的信息&#xff0c;用json.loads()解析&#xff0c;出报一下错误&#xff1a;Traceback (most recent call last):File "test1.py", line 12, in decodejson json.loads(str1)File "/usr/…

用计算机心如止水,计算机网络数据链路层

1、数据链路层使用的信道类型&#xff1a;点对点信道(1对1)广播信道(1对多)&#xff0c;如&#xff1a;集线器、交换机2、链路与数据链路&#xff1a;链路&#xff1a;点对点的物理线路段&#xff0c;中间无任何其他交换结点数据链路&#xff1a;通信协议 软件 硬件(一般用适…

Java中Integer类的方法

java.lang 类 Integerjava.lang.Objectjava.lang.Numberjava.lang.Integer全部已实现的接口&#xff1a; Serializable, Comparable<Integer> public final class Integerextends Numberimplements Comparable<Integer>Integer 类在对象中包装了一个基本类型 int 的…

openfeign ribbon 负载_SpringCloud教程(三)声明式访问Feign、负载均衡Ribbon

0-前言eureka实际上已经集成了负载均衡调度框架Ribbon&#xff1b;我们有了各个微服务了&#xff0c;那怎么来调用他们呢&#xff0c;一种方法是可以使用 RestTemplate(如&#xff1a;String str restTemplate.getForObject(“http://localhost:8763/user/hello”,String.class…

HTML5的little,HTML 5

The following example, the top ten movies are listed (in reverseorder). Note the way the list is given a title by using aThe top 10 movies of all timeJosie and the Pussycats, 2001Црна мачка, бели мачор, 1998A Bugs Life, 1998Toy Story, 199…

MYC编译器源码分析之程序入口

前文.NET框架源码解读之MYC编译器讲了MyC编译器的架构&#xff0c;整个编译器是用C#语言写的&#xff0c;上图列出了MyC编译器编译一个C源文件的过程&#xff0c;编译主路径如下&#xff1a; 首先是入口Main函数用来解析命令行参数&#xff0c;读取源文件&#xff0c;并开始编译…

easyui tree动态加载_动态路由:Gin vs SpringMVC

这两天在接触Gin&#xff0c;对它的动态路由功能比较感兴趣&#xff0c;特意做了笔记&#xff0c;顺便跟SpringMVC作下对比。1.简介Gin是使用Go/golang语言实现的HTTP Web框架。接口简洁,性能极高。截止1.4.0版本,包含测试代码,仅14K,其中测试代码9K左右,也就是说框架源码仅5K左…

伍德里奇---计量经济学第6章部分计算机习题详解(stata),伍德里奇---计量经济学第6章部分计算机习题详解(STATA)...

所以&#xff0c;log bwg t7.5840.0180npvis?0.00041npvis20.0254mage?0.00041mage20.1370.00370.000120.0093 (0.00015)n1764&#xff0c;R20.0256&#xff0c;R20.0234。当孩子的出生体重最大时&#xff0c;对应的年龄为mage? β12β20.0254[2?0.00041] 30.96&#xff0c…

经验原石_哔哩哔哩(b站)手机版经验原石怎么样获得?在哪里使用?有期限吗?...

大家都知道原石是一种道具&#xff0c;但是怎么用&#xff0c;有什么用大家却不太清楚接下来我就给大家介绍一下原石有什么用。那就是原石是可以用来增我们加头衔经验的&#xff0c;大家都知道头衔有两种&#xff0c;一种是可以升级的&#xff0c;一种是不可以升级的。我们的这…

vue菜单点击html,基于vue.js实现侧边菜单栏

侧边菜单栏应该是很多项目里必不可少的 自己手写了一个下面是效果图下面就说一下实现的过程 还是比较简单的首先导入一下需要的文件接下来就是我的html主页系统用户管理权限设置操作日志设备运行日志防区 防区管理警报 历史报警曲线报表统计警情处理设备然后是css* {margin: 0;…

centos7 mysql启动失败_Mysql主从复制

​好在&#xff0c;别人如何分析我&#xff0c;跟我本身是一点关系也没有的。(by 三毛)主从复制的常见用途1.数据备份对备份来说,复制是一项很有意义的技术补充,但复制既不是备份也不能取代备份2.负载均衡(读写分离)通过MySQL主从复制,可以把写操作放在主库(master)&#xff0c…

【原创】shadowebdict开发日记:基于linux的简明英汉字典(三)

全系列目录&#xff1a; 【原创】shadowebdict开发日记&#xff1a;基于linux的简明英汉字典&#xff08;一&#xff09;【原创】shadowebdict开发日记&#xff1a;基于linux的简明英汉字典&#xff08;二&#xff09;【原创】shadowebdict开发日记&#xff1a;基于linux的简明…

计算机应用基础知道,谁知道计算机应用基础练习题的答案?

谁知道计算机应用基础练习题的答案&#xff1f;46&#xff0e;简述美籍匈牙利数学家冯诺依曼(Von Neumann)在1946年提出的关于计算机的体系结构和“程序存储”的设计思想。47&#xff0e;简要说明Windows98中安装存放在CD—ROM上的应用程序的操作过程。48&#xff0e;简述Windo…

springboot分页展示功能_springboot实现分页

这里项目是springboot&#xff0c;mybatis&#xff0c;thymeleaf&#xff0c;实现我自制的商城项目后台页面的分页功能。参考我之前总结的分页&#xff0c;简单的处理一下就可以了上代码首先写一个超链接跳转到controller层的方法商品管理然后写后台controller层代码/*** 后台商…

Delphi中的基础数据类型

参考http://www.cnblogs.com/del/archive/2007/12/04/982167.html 在学习之初&#xff0c;在这么多的数据类型中&#xff0c;最好记住这五种标准数据类型&#xff08;整型、实型、字符型、字符串型、布尔类型&#xff09;和这五种类型中的最常用的类型&#xff0c;其他类型在以…

计算机程序编辑器中文版,小说编辑器(Atomic Scribbler)

AtomicScribbler是一款功能强大的电脑小说编辑器&#xff0c;快速编辑、创作你的小说&#xff0c;也可以直接导入文本二次编辑&#xff0c;内置了丰富的文字编辑工具&#xff0c;完全能够满足日常创作小说、编辑文字使用需求。。相关软件软件大小版本说明下载地址AtomicScribbl…

信号与系统sa函数求积分_瞎扯数学分析微积分(1~4)

算法数学之美日期&#xff1a;2019年10月30日正文共&#xff1a;12013字0图预计阅读时间&#xff1a;31分钟来源:豆瓣 wxmang这一篇帖子主要介绍人类如何从一个基于几何直观或直觉的计算技巧或计算方法&#xff0c;进化到逻辑基础严密的公理体系的例子&#xff0c;想说明人类抽…