Pythonの並列処理・並行処理のための標準モジュールの比較

Pythonで並列処理・並行処理を提供する標準モジュールは数多くあり、初めてだと違いを理解するのは困難です。この記事では、それぞれの違いについて調べました。

threadモジュール(Python 2), _threadモジュール(Python 3)

かつてPython 2にはthreadモジュールという複数のスレッドを扱うためのモジュールが存在していましたが、Python 3でdeprecated扱いになりました。一応_threadモジュールという名前で残っています。公式でも述べられているように、一般には、thread/_threadモジュールではなく、より高レベルなthreadingモジュールの使用が推奨されるようです。

threadingモジュール

threadingモジュールは、先述の通り、複数のスレッドを扱うためのモジュールです。thread/_threadモジュールより高レベルとはいうものの、この後に紹介するモジュールに比べるとまだまだ低レベルで、C++11のthreadライブラリと同程度の印象を受けます。

コード例を以下に示します。threading.Threadクラスを継承したクラスを作るのが常套手段のようです。

#!/usr/bin/python3

import threading
import time

class MyThread(threading.Thread):
   def __init__(self, name, sleep_time):
      threading.Thread.__init__(self)
      self.name = name
      self.sleep_time = sleep_time
   def run(self):
      print ("Starting " + self.name)
      time.sleep(self.sleep_time)
      print ("Exiting " + self.name)

thread_num = 3
threads = []
for i in range(thread_num):
    threads.append(MyThread("Thread-{}".format(i), 5 - i))

for th in threads:
    th.start()

for th in threads:
    th.join()

print("end")

この例では、3つのスレッドをstart()でほぼ同時に立ち上げます。3つのスレッドは、それぞれ5秒、4秒、3秒待機したのちに終了します。すべてのスレッドが終了するのをjoin()で待ってからプログラムを終了します。このプログラムを実行するとちょうど約5秒で終わることが確かめられます。

ここで、Pythonでのマルチスレッド処理は、C++とのマルチスレッド処理とは大きく異なることを知っておくのは重要です。Pythonの主要な実装系の一つであるCPythonにはGIL(Global Interpreter Lock)という機構があり、複数のスレッドが同時にPythonバイトコードを実行することを許しません(参考:GlobalInterpreterLock - Python Wiki)。なので、例えばCPU速度がボトルネックになるような重い計算処理(いわゆるCPU boundな処理)を、このthreadingを使って複数のスレッドに割り振って動かしたとしても、実際にはGILの制約のために複数のスレッドが同時に実行されることはなく、処理時間は期待したように短くならないはずです。

一方、ディスクの読み出し・書き込みなどのI/O待ち時間が大量に発生するような処理(いわゆるI/O boundな処理)であれば、GILは問題にならないので、このthreadingを使ってマルチスレッド化することで処理時間が早くなる可能性があります。

threadingモジュールは後述する他のモジュールに比べて自由度が高い分、デッドロックやデータ競合が起こらないように十分考慮してプログラムを組む必要があります。以下は、リソースをロックする順番をめぐってデッドロックしてしまう例です。

# cf. http://dabeaz.blogspot.jp/2009/11/python-thread-deadlock-avoidance_20.html

import threading

a_lock = threading.Lock()
b_lock = threading.Lock()

def foo():
    with a_lock:
         with b_lock:
            print ("a -> b")

def bar():
    with b_lock:
         with a_lock:
            print ("b -> a")

class MyThread(threading.Thread):
   def __init__(self, func):
      threading.Thread.__init__(self)
      self.func = func
   def run(self):
      while True:
         self.func()

th1 = MyThread(foo)
th2 = MyThread(bar)

th1.start()
th2.start()

th1.join()
th2.join()

multiprocessingモジュール

multiprocessingモジュールは、複数のプロセスを扱うためのモジュールです。スレッドの代わりにサブプロセスを立ち上げてそちらで処理させることで、GILの問題を回避することができます。ただし、サブプロセスの立ち上げは、スレッドの立ち上げに比べると重い処理なので、本当にプロセス単位での並列化が必要なのか、言い換えると、スレッド単位の並列化で十分ということはないか、一考が必要です。

multiprocessing.Process

multiprocessing.Processを使った例を以下に示します。先に示した、threading.Threadクラスを用いた例と使い方は同じです。こちらもプログラマが使い方を誤るとデッドロックやデータ競合を引き起こすので、要注意です。

#!/usr/bin/python3

import multiprocessing
import time

class MyProcess(multiprocessing.Process):
   def __init__(self, name, sleep_time):
      multiprocessing.Process.__init__(self)
      self.name = name
      self.sleep_time = sleep_time
   def run(self):
      print ("Starting " + self.name)
      time.sleep(self.sleep_time)
      print ("Exiting " + self.name)

process_num = 3
processs = []
for i in range(process_num):
    processs.append(MyProcess("Process-{}".format(i), 5 - i))

for th in processs:
    th.start()

for th in processs:
    th.join()

print("end")

multiprocessing.Pool

さらに、multiprocessingモジュールは、「データを複数プロセスにばらまいて、複数プロセスで計算させ、結果を集める」(fork-join)という、並列処理でよくあるユースケースを実現する専用のAPIを追加で提供しています。それがmultiprocessing.Poolです。

例えば、「実数からなるあるリストが与えられたとき、リストの各要素を2乗したリストを出力」する処理を、multiprocessing.Poolを用いて4プロセス並列で実行する例を以下に示します。

import multiprocessing

def pow2(n):
   return n * n

before = list(range(100000000))
with multiprocessing.Pool(4) as p:
   after = p.map(pow2, before)

print(before[:5]) # [0, 1, 2, 3, 4]
print(after[:5]) # [0, 1, 4, 9, 16]

このプログラムを実行中にpsコマンドなどでプロセスを見ると、メインプロセスに加えてサブプロセスが4つ立ち上がっているのを観察できると思います。

より複雑な機能に関しては公式ドキュメントをご覧ください。

multiprocessing.dummy.Pool

multiprocessingには、あまり知られていないmultiprocessing.dummy.Poolモジュールが存在しています。前の節で紹介したmultiprocessing.Poolはプロセス単位で処理を並列化したのに対し、このmultiprocessing.dummy.Poolはスレッド単位で処理を並列化します。ともにAPIは同じです。

前節のコードを、スレッド単位で並列化するように変更した例を以下に示します。

import multiprocessing.dummy

def pow2(n):
   return n * n

before = list(range(100000000))
with multiprocessing.dummy.Pool(4) as p:   # この行が変わっただけです
   after = p.map(pow2, before)

print(before[:5])
print(after[:5])

プロセス単位での並列化と、スレッド単位での並列化を、1行の書き換えのみで簡単に切り替えられるので、並列化の対象となる処理がCPU boundかI/O boundかを実際に確かめたいときに使えるテクニックだと思います。参考:multithreading - How to use threading in Python? - Stack Overflow

concurrent.futuresモジュール

Python3.2からconcurrent.futuresモジュールが提供されるようになりました。Python 2.x系でもPyPIから同名のパッケージを取得可能です。

これまで見てきたようなマルチスレッドやマルチプロセスの処理を隠蔽して、複数の処理を同時に行うための抽象度の高い機能を提供します。具体的には、Futureと呼ばれるクラスを提供することで、「非同期処理が完了した状態、または、未完了の状態」を表すことができるようになります。このモジュールの導入の経緯については、PEP 3148 -- futures - execute computations asynchronously | Python.org に詳しいです。

concurrent.futuresが提供するメインの機能は、futures.ThreadPoolExecutorfutures.ProcessPoolExecutorです。それぞれ、マルチスレッド処理、マルチプロセス処理を扱いたい時に用います。まず、futures.ProcessPoolExecutorを使って、1000個のタスクをプロセス並列で同時に実行する例を以下に示します。(最初はmultiprocessing.Pool()の例と同じくサイズ1億で試したのですが、メモリ使用量が際限なく増えてしまいました)

from concurrent import futures

def pow2(n):
   return n * n

before = list(range(1000))
with futures.ProcessPoolExecutor(max_workers=4) as executor:
   after = executor.map(pow2, before)

print(before[:5])
print(after[:5])

これは今まで見てきたmultiprocessing.Pool()とかなり似た書き方です。

上の例ではFutureという概念は隠蔽されていてますが、陽に扱うこともできます。以下に、map()を使わずFutureオブジェクトを用いて並列処理する例を示します。

#!/usr/bin/python3

from concurrent import futures

def pow2(n):
   return n * n

before = list(range(1000))
with futures.ThreadPoolExecutor(max_workers=4) as executor:

   print("submission starts")
   to_do = []
   for num in before:
      future = executor.submit(pow2, num)
      to_do.append(future)
   print("submission ends")

   after = []
   for future in futures.as_completed(to_do):
      res = future.result()
      after.append(res)

print(before[:5])
print(after[:5])

実行例は以下です。処理順は不定です。

submission starts
submission ends
[0, 1, 2, 3, 4]
[262144, 244036, 122500, 163216, 280900]

concurrent.futuresモジュール

concurrent.futuresモジュールはPython 3.4から導入された、イベントループに基づく非同期処理を行うためのモジュールです。

このモジュールは公式ドキュメントの量が半端ではなく、自分もあまり理解できていないため、紹介のみにとどめます。

まとめ

以下の方針でモジュールを選ぶのがよいと思います。

  • 処理はI/O boundである
    • 処理はfork-joinモデルで並列化できる
      • multiprocessing.dummy.Pool を使う
      • → または、 futures.ThreadPoolExecutormap関数を使う
    • 処理はもっと複雑
      • futures.ThreadPoolExecutorsubmit関数を使って、タスク単位に処理を行う
      • → または、threading を使って、さらに柔軟にモデルを組み立てる
  • 処理はCPU boundである
    • 処理はfork-joinモデルで並列化できる
      • multiprocessing.Pool を使う
      • → または、 futures.ProcessPoolExecutormap関数を使う
    • 処理はもっと複雑
      • futures.ProcessPoolExecutorsubmit関数を使って、タスク単位に処理を行う
      • → または、multiprocessing を使って、さらに柔軟にモデルを組み立てる

参考URL

Makefileの書き方に関する備忘録 その4

他にもMakefileに関する記事を書いていますが、この記事だけで読んでも問題ありません。目次→Makefileの書き方に関する備忘録 - minus9d's diary

少し複雑な構成を持つC++のコード群からバイナリをビルドするための良いMakefileの例を makefile - How to place object files in separate subdirectory - Stack Overflow に見つけたので共有します。

目標

まず、以下のようなソースコードを持っているとします。srcC++ソースコードを格納するディレクトリで、その下のディレクトリ構造は決まった形をもたないものとします。includeはヘッダファイルのみを格納するディレクトリです。

/your_project
  /src
     /subdir1/a.cpp, a.h
              b.cpp, b.h
     /subdir2/subsubdir/c.cpp, c.h
     d.cpp, d.h
  /include
     e.h

これをビルドして以下のようにするのを目標とします。

/your_project
  /src
  /include
  /obj  中間生成物置き場
  /bin  最終バイナリ置き場 

Makefile

前出のページを参考に、自分なりに取捨選択して書き直したMakefileが以下です。このファイルは、your_projectディレクトリの直下に置かれているものとします。

# 参考:https://stackoverflow.com/questions/5178125/how-to-place-object-files-in-separate-subdirectory

# C++のコンパイラ
CXX          := g++

# 生成するバイナリの名前
TARGET      := target

# ディレクトリ名
SRCDIR      := src
BUILDDIR    := obj
TARGETDIR   := bin

# 拡張子名
SRCEXT      := cpp
DEPEXT      := d
OBJEXT      := o

# コンパイル、リンクのフラグ
CXXFLAGS    := -Wall -g -std=c++14
LDFLAGS     :=
INC         := -Isrc -Iinclude

#---------------------------------------------------------------------------------
# DO NOT EDIT BELOW THIS LINE
#---------------------------------------------------------------------------------

sources     := $(shell find $(SRCDIR) -type f -name *.$(SRCEXT))
objects     := $(patsubst $(SRCDIR)/%,$(BUILDDIR)/%,$(subst $(SRCEXT),$(OBJEXT),$(sources)))
dependencies := $(subst $(OBJEXT),$(DEPEXT),$(objects))

# Defauilt Make
all: directories $(TARGETDIR)/$(TARGET)

# Remake
remake: cleaner all

# ディレクトリ生成
directories:
    @mkdir -p $(TARGETDIR)
    @mkdir -p $(BUILDDIR)

# 中間生成物のためのディレクトリを削除
clean:
    @$(RM) -rf $(BUILDDIR)

# 中間生成物のためのディレクトリと最終生成物のためのディレクトリを削除
cleaner: clean
    @$(RM) -rf $(TARGETDIR)

# 自動抽出した.dファイルを読み込む
-include $(dependencies)

# オブジェクトファイルをリンクしてバイナリを生成
$(TARGETDIR)/$(TARGET): $(objects)
    $(CXX) -o $(TARGETDIR)/$(TARGET) $^ $(LDFLAGS)

# ソースファイルのコンパイルしてオブジェクトファイルを生成
# また、ソースファイルの依存関係を自動抽出して.dファイルに保存
$(BUILDDIR)/%.$(OBJEXT): $(SRCDIR)/%.$(SRCEXT)
    @mkdir -p $(dir $@)
    $(CXX) $(CXXFLAGS) $(INC) -c -o $@ $<
    @$(CXX) $(CXXFLAGS) $(INC) -MM $(SRCDIR)/$*.$(SRCEXT) > $(BUILDDIR)/$*.$(DEPEXT)
    @cp -f $(BUILDDIR)/$*.$(DEPEXT) $(BUILDDIR)/$*.$(DEPEXT).tmp
    @sed -e 's|.*:|$(BUILDDIR)/$*.$(OBJEXT):|' < $(BUILDDIR)/$*.$(DEPEXT).tmp > $(BUILDDIR)/$*.$(DEPEXT)
    @sed -e 's/.*://' -e 's/\\$$//' < $(BUILDDIR)/$*.$(DEPEXT).tmp | fmt -1 | sed -e 's/^ *//' -e 's/$$/:/' >> $(BUILDDIR)/$*.$(DEPEXT)
    @rm -f $(BUILDDIR)/$*.$(DEPEXT).tmp

# Non-File Targets
.PHONY: all remake clean cleaner

このMakefileでは、"DO NOT EDIT BELOW THIS LINE" より上にある項目を自分の環境に合わせて変更し、makeとコマンドを打つことで、バイナリがbin以下にできるようになります。

また、ソースコードの依存関係も自動で抽出するので、あるファイルを更新したあとmakeすると、そのファイルに依存するすべてのファイルに再コンパイルがかかります。

使い方は以下の通りです。

Pillowで読み込んだ画像に対してChainerCVの検出器を実行

今年の8月、PFNからGitHub - chainer/chainercv: ChainerCV: a Library for Computer Vision in Deep Learningがリリースされました。今のところ2つの物体検出手法(Faster R-CNN, SSD)と1つの画像セグメンテーション手法(SemSeg)が実装されています。examplesフォルダにあるサンプルファイルを実行するだけでお手軽に物体検出を試せるようになっていて、敷居が下がっています。

しかしながら、公式サンプルでは、入力画像を読み込むのに`chainercv.utils.read_image()を使う方法しか示されていないようです。そこで、この記事では、Pillowで読み込んだ画像に対してFaster R-CNNを実行し、自前で検出結果を画像に重畳するサンプルを示したいと思います。

サンプルコード

以下の通りです。Ubuntu 16.04 + Python 3.6で動作を確認しています。

# -*- coding: utf-8 -*-

import argparse

import chainer
from chainercv.datasets import voc_detection_label_names
from chainercv.links import FasterRCNNVGG16
import numpy as np
from PIL import Image, ImageDraw, ImageFont


def convert_pilimg_for_chainercv(pilimg):
    """ pilimg(RGBのカラー画像とする)を、ChainerCVが扱える形式に変換 """ 
    img = np.asarray(pilimg, dtype=np.float32)
    # transpose (H, W, C) -> (C, H, W)
    return img.transpose((2, 0, 1))

def overlay(pilimg, bbox, label, score, label_names=voc_detection_label_names):
    """ pilimgに物体検出結果を重畳。vis_bbox.py を参考に実装 """

    draw = ImageDraw.Draw(pilimg)
    fnt = ImageFont.truetype('Pillow/Tests/fonts/FreeMono.ttf', 20)

    for i, bb in enumerate(bbox):
        y0, x0, y1, x1 = bb
        draw.rectangle([x0, y0, x1, y1], fill=None, outline='red')

        caption = list()

        if label is not None and label_names is not None:
            lb = label[i]
            if not (0 <= lb < len(label_names)):
                raise ValueError('No corresponding name is given')
            caption.append(label_names[lb])

        if score is not None:
            sc = score[i]
            caption.append('{:.2f}'.format(sc))

        if len(caption) > 0:
            message = ': '.join(caption)
            # テキストを表示するための矩形サイズを取得
            text_width, text_height = fnt.getsize(message)
            # テキストの背後を白く塗る
            draw.rectangle([x0, y0, x0 + text_width, y0 + text_height],
                           fill=(255, 255, 255, 128))
            # テキストを重畳
            draw.text((x0, y0), message, font=fnt, fill='black')


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument('--gpu', type=int, default=-1)
    parser.add_argument('--pretrained_model', default='voc07')
    parser.add_argument('image')
    args = parser.parse_args()

    model = FasterRCNNVGG16(
        n_fg_class=len(voc_detection_label_names),
        pretrained_model=args.pretrained_model)

    if args.gpu >= 0:
        chainer.cuda.get_device_from_id(args.gpu).use()
        model.to_gpu()

    # PILを使って画像を読み込み
    pilimg = Image.open(args.image).convert('RGB')
    # ChainerCVが扱える形式に変換
    img = convert_pilimg_for_chainercv(pilimg)
    # 物体を検出
    bboxes, labels, scores = model.predict([img])
    bbox, label, score = bboxes[0], labels[0], scores[0]
    # 検出結果を重畳して表示
    overlay(pilimg, bbox, label, score, label_names=voc_detection_label_names)
    pilimg.show()
    # 結果を保存
    pilimg.save('result.jpg')

if __name__ == '__main__':
    main()

処理結果が表示された後、result.jpgという名前で保存されます。 自前で重畳処理をした結果を以下に示します。

f:id:minus9d:20170919223928j:plain

argparseで引数の個数を指定する

Pythonの標準ライブラリargparseで、nargsを使うと、オプションがとる引数の個数を指定できます。例えば、

import argparse
parser = argparse.ArgumentParser()
parser.add_argument("--arg1", nargs=3)
args = parser.parse_args()

if args.arg1:
    print(args.arg1)

とすると、オプションarg1はちょうど3個の引数を要求することを意味します。オプションの使用時には、

$ python prog1.py --arg1 a b c

と書けます。このとき、args.arg1には['a', 'b', 'c']が入ります。

同様に、

  • nargs='?':0個または1個の引数を要求
  • nargs='*':0個以上の引数を要求
  • nargs='+':1個以上の引数を要求

という意味になります。

Visual C++にて、CPUにAVX命令があるかどうかを実行中に知る方法

表題のことは、__cpuid, __cpuidexにあるサンプルコードで実現できます。Visual Studio 2017で確認しました。

自分の初代Core i7では、下記のようにAVXに対応していないことがわかります。

GenuineIntel
Intel(R) Core(TM) i7 CPU         870  @ 2.93GHz
3DNOW not supported
3DNOWEXT not supported
ABM not supported
ADX not supported
AES not supported
AVX not supported
AVX2 not supported
AVX512CD not supported
AVX512ER not supported
AVX512F not supported
AVX512PF not supported
BMI1 not supported
BMI2 not supported
CLFSH supported
CMPXCHG16B supported
CX8 supported
ERMS not supported
F16C not supported
FMA not supported
FSGSBASE not supported
FXSR supported
HLE not supported
INVPCID not supported
LAHF supported
LZCNT not supported
MMX supported
MMXEXT not supported
MONITOR supported
MOVBE not supported
MSR supported
OSXSAVE not supported
PCLMULQDQ not supported
POPCNT supported
PREFETCHWT1 not supported
RDRAND not supported
RDSEED not supported
RDTSCP supported
RTM not supported
SEP supported
SHA not supported
SSE supported
SSE2 supported
SSE3 supported
SSE4.1 supported
SSE4.2 supported
SSE4a not supported
SSSE3 supported
SYSCALL not supported
TBM not supported
XOP not supported
XSAVE not supported

g++によるhello world再訪

g++でhello.cppから実行ファイルを生成するときに何が起こっているかを調べてまとめました。全体的に gcc Compilation Process and Steps of C Program in Linux を参考にしています。元記事はCファイルを対象にしていますが、本記事ではC++ファイルを対象としています。

ソースコード

以下のコード hello.cpp をステップバイステップでビルドしていきます。このコードはC++11相当のコードです。

#include <iostream>
#include <vector>
#define MESSAGE "hello  "

int main(void)
{
    std::vector<int> vec{1,2,3,4,5};
    for(auto e: vec) {
        std::cout << MESSAGE << __FILE__ << " " << e << std::endl;
    }
    
    return 0;
}

1. プリプロセッシング

プリプロセッシングの結果を見る

まず最初に、プリプロセッシングと呼ばれる前処理が行われます。具体的には、

  • #includeでインクルードしたヘッダファイルの展開
  • #defineで定義した文字列の置換処理
  • __FILE____LINE__などの記号定数の置換処理

が行われます。(参考:マクロ展開の例

$ cpp -x c++ -std=c++11 hello.cpp > hello.i

または

$ g++ -E -std=c++11 hello.cpp > hello.i

で、プロプロセッシング後のファイルをhello.iという名前で保存できます。

hello.iの抜粋を以下に示します。

# 1 "hello.cpp"
# 1 "<built-in>"
# 1 "<command-line>"
# 1 "/usr/include/stdc-predef.h" 1 3 4
# 1 "<command-line>" 2
# 1 "hello.cpp"
# 1 "/usr/include/c++/5/iostream" 1 3
# 36 "/usr/include/c++/5/iostream" 3
...(中略)...
# 5 "hello.cpp"
int main(void)
{
    std::vector<int> vec{1,2,3,4,5};
    for(auto e: vec) {
        std::cout << "hello  " << "hello.cpp" << " " << e << std::endl;
    }

    return 0;
}

ヘッダファイルが検索されるディレクトリの場所を調べる

ヘッダファイルが捜索されるディレクトリの場所は、Cの場合は

$ gcc -xc -E -v -

C++の場合は

$ gcc -xc++ -E -v -

で調べることができます(参考:c++ - What are the GCC default include directories? - Stack Overflow)。私の場合は以下のように表示されました。

()
#include "..." search starts here:
#include <...> search starts here:
 /usr/include/c++/5
 /usr/include/x86_64-linux-gnu/c++/5
 /usr/include/c++/5/backward
 /usr/lib/gcc/x86_64-linux-gnu/5/include
 /usr/local/include
 /usr/lib/gcc/x86_64-linux-gnu/5/include-fixed
 /usr/include/x86_64-linux-gnu
 /usr/include
End of search list.

includeされるヘッダファイルのフルパスを調べる

ヘッダファイルは芋づる式にincludeされていくわけですが、具体的にどのパスにあるどのヘッダファイルがincludeされたかは、

$ g++ -std=c++11 -H hello.cpp

で調べることができます(参考:c++ - How to tell where a header file is included from? - Stack Overflow)。私の環境だと以下のようにツリーが表示されました。

$ g++ -H hello.cpp       
. /usr/include/c++/5/iostream
.. /usr/include/x86_64-linux-gnu/c++/5/bits/c++config.h
... /usr/include/x86_64-linux-gnu/c++/5/bits/os_defines.h
.... /usr/include/features.h
..... /usr/include/x86_64-linux-gnu/sys/cdefs.h
...... /usr/include/x86_64-linux-gnu/bits/wordsize.h
..... /usr/include/x86_64-linux-gnu/gnu/stubs.h
...... /usr/include/x86_64-linux-gnu/gnu/stubs-64.h
...(略)...

2. アセンブリコードへの変換

次に、コンパイラは、hello.iアセンブリコードに変換します。

$ g++ -std=c++11 -S hello.i

とすると、アセンブリコードhello.sが生成されます。

        .file   "hello.cpp"
        .section        .rodata
        .type   _ZStL19piecewise_construct, @object
        .size   _ZStL19piecewise_construct, 1
_ZStL19piecewise_construct:
        .zero   1
        .local  _ZStL8__ioinit
        .comm   _ZStL8__ioinit,1,1
...(略)...

3. オブジェクトファイルへの変換

次に、asコマンドを使って、アセンブリコードhello.sからマシン語に変換し、オブジェクトファイルhello.oを得ます。

$ as hello.s -o hello.o

hello.oの中身は

$ objdump -d hello.o

で調べることができます。以下は実行例です。

0000000000000000 <main>:
   0:   55                      push   %rbp
   1:   48 89 e5                mov    %rsp,%rbp
   4:   41 55                   push   %r13
   6:   41 54                   push   %r12
   8:   53                      push   %rbx
   9:   48 83 ec 58             sub    $0x58,%rsp
   d:   64 48 8b 04 25 28 00    mov    %fs:0x28,%rax
...(略)...

4. リンク

実行ファイルを生成する

最後に、さっき生成したオブジェクトファイルhello.oおよび、他に必要なオブジェクトファイルを集めてリンクし、実行ファイルを生成します。

$ g++ hello.o -o hello

とすると、実行ファイルhelloが生成されます。

ライブラリが検索されるディレクトリの場所、リンクされるライブラリのフルパスを調べる

$ g++ -v hello.o -o hello

とすると、以下のように詳細情報が表示されます。

LIBRARY_PATH=/usr/lib/gcc/x86_64-linux-gnu/5/:/usr/lib/gcc/x86_64-linux-gnu/5/../../../x86_64-linux-gnu/:/usr/lib/gcc/x86_64-linux-gnu/5/../../../../lib/:/lib/x86_64-linux-gnu/:/lib/../lib/:/usr/lib/x86_64-linux-gnu/:/usr/lib/../lib/:/usr/lib/gcc/x86_64-linux-gnu/5/../../../:/lib/:/usr/lib/

は、ライブラリが検索されるディレクトリの場所を表していると思います。

また、

COLLECT_GCC_OPTIONS='-v' '-o' 'hello' '-shared-libgcc' '-mtune=generic' '-march=x86-64'
 /usr/lib/gcc/x86_64-linux-gnu/5/collect2 -plugin /usr/lib/gcc/x86_64-linux-gnu/5/liblto_plugin.so -plugin-opt=/usr/lib/gcc/x86_64-linux-gnu/5/lto-wrapper -plugin-opt=-fresolution=/tmp/ccvdOxv1.res -plugin-opt=-pass-through=-lgcc_s -plugin-opt=-pass-through=-lgcc -plugin-opt=-pass-through=-lc -plugin-opt=-pass-through=-lgcc_s -plugin-opt=-pass-through=-lgcc --sysroot=/ --build-id --eh-frame-hdr -m elf_x86_64 --hash-style=gnu --as-needed -dynamic-linker /lib64/ld-linux-x86-64.so.2 -z relro -o hello /usr/lib/gcc/x86_64-linux-gnu/5/../../../x86_64-linux-gnu/crt1.o /usr/lib/gcc/x86_64-linux-gnu/5/../../../x86_64-linux-gnu/crti.o /usr/lib/gcc/x86_64-linux-gnu/5/crtbegin.o -L/usr/lib/gcc/x86_64-linux-gnu/5 -L/usr/lib/gcc/x86_64-linux-gnu/5/../../../x86_64-linux-gnu -L/usr/lib/gcc/x86_64-linux-gnu/5/../../../../lib -L/lib/x86_64-linux-gnu -L/lib/../lib -L/usr/lib/x86_64-linux-gnu -L/usr/lib/../lib -L/usr/lib/gcc/x86_64-linux-gnu/5/../../.. hello.o -lstdc++ -lm -lgcc_s -lgcc -lc -lgcc_s -lgcc /usr/lib/gcc/x86_64-linux-gnu/5/crtend.o /usr/lib/gcc/x86_64-linux-gnu/5/../../../x86_64-linux-gnu/crtn.o

は、リンク時に使われたコマンドの詳細を表していると思います。collect2とはgcc/g++が内部で呼び出すリンクのためのコマンドのようです。ldとの違いはよくわかりません…。

その他の情報

$ g++ -save-temps -std=c++11 hello.cpp

とすると、上記でステップバイステップで生成した中間ファイル hello.ii, hello.s, hello.o が一気に生成されます。

Ubuntuの「壊れた変更禁止パッケージがあります」エラーにaptitudeで対処する方法

Ubuntu 16.04にて、少し前からapt-getで新しいパッケージを入れようとすると「壊れた変更禁止パッケージがあります」エラーが出てインストールできない事象に悩まされていました。

$ sudo apt-get install libgfortran3  
パッケージリストを読み込んでいます... 完了
依存関係ツリーを作成しています                
状態情報を読み取っています... 完了
インストールすることができないパッケージがありました。おそらく、あり得
ない状況を要求したか、(不安定版ディストリビューションを使用しているの
であれば) 必要なパッケージがまだ作成されていなかったり Incoming から移
動されていないことが考えられます。
以下の情報がこの問題を解決するために役立つかもしれません:

以下のパッケージには満たせない依存関係があります:
 libgfortran3 : 依存: gcc-5-base (= 5.3.1-14ubuntu2) しかし、5.4.0-6ubuntu1~16.04.4 はインストールされようとしています
E: 問題を解決することができません。壊れた変更禁止パッケージがあります。

aptitudeを使ってこの問題への対応を試みました(参考:apt - E: Unable to correct problems, you have held broken packages - Ask Ubuntu)。以下にその方法について記します。自分でもこれが正しい方法なのかよくわかっていません。試す場合は自己責任でお願いします。

$ sudo aptitude install libgfortran3

とすると、以下のように、壊れたパッケージを修正するための改善案が表示されます。改善案を受け入れるならY、他の改善案を探すならnです。

以下の新規パッケージがインストールされます:
  libgfortran3{b} 
0 個のパッケージを更新、 1 個を新たにインストール、 0 個を削除予定、0 個が更新されていない。
260 k バイトのアーカイブを取得する必要があります。 展開後に 1,290 k バイトのディスク領域が新たに消費されます。
以下のパッケージには満たされていない依存関係があります:
 libgfortran3 : 依存: gcc-5-base (= 5.3.1-14ubuntu2) [5.4.0-6ubuntu1~16.04.4 が既にインストール済みです]
以下のアクションでこれらの依存関係の問題は解決されます:

     以下のパッケージを現在のバージョンに一時固定する:
1)     libgfortran3 [インストールされていません]      



この解決方法を受け入れますか? [Y/n/q/?]

私の場合、Yとしてもパッケージをインストールできなかったので、nで次の改善案を探しました。その結果が以下です。

以下のアクションでこれらの依存関係の問題は解決されます:

      以下のパッケージを削除する:                                              
1)      build-essential                                                        
2)      g++                                                                    
3)      g++-5                                                                  
4)      gcc                                                                    
5)      gcc-5                                                                  
6)      libasan2                                                               
7)      libatomic1                                                             
8)      libcilkrts5                                                            
9)      libgcc-5-dev                                                           
10)     libitm1                                                                
11)     liblsan0                                                               
12)     libmpx0                                                                
13)     libstdc++-5-dev                                                        
14)     libtsan0                                                               
15)     libubsan0                                                              

      以下のパッケージをインストールする:                                      
16)     tcc [0.9.27~git20151227.933c223-1 (xenial)]                            

      以下のパッケージをダウングレードする:                                    
17)     cpp-5 [5.4.0-6ubuntu1~16.04.4 (now) -> 5.3.1-14ubuntu2 (xenial)]       
18)     gcc-5-base [5.4.0-6ubuntu1~16.04.4 (now) -> 5.3.1-14ubuntu2 (xenial)]  
19)     libcc1-0 [5.4.0-6ubuntu1~16.04.4 (now) -> 5.3.1-14ubuntu2 (xenial)]    
20)     libgomp1 [5.4.0-6ubuntu1~16.04.4 (now) -> 5.3.1-14ubuntu2 (xenial)]    
21)     libquadmath0 [5.4.0-6ubuntu1~16.04.4 (now) -> 5.3.1-14ubuntu2 (xenial)]
22)     libstdc++6 [5.4.0-6ubuntu1~16.04.4 (now) -> 5.3.1-14ubuntu2 (xenial)]  

      以下の依存関係を未解決のままにする:                                      
23)     codeblocks が gcc | g++ を推奨                                         
24)     dpkg-dev が build-essential を推奨                                     
25)     cmake が gcc を推奨                                                    


この解決方法を受け入れますか? [Y/n/q/?] 

ここでYとすると、無事所望のパッケージをインストールできました。しかし、今度はgccとg++がアンインストールされ、利用できなくなってしまいました。

sudo aptitude install g++

以下の新規パッケージがインストールされます:
  g++ g++-5{a} gcc{a} gcc-5{a} libasan2{a} libatomic1{a} libc-dev-bin{a} 
  libc6-dev{ab} libcilkrts5{a} libgcc-5-dev{a} libitm1{a} liblsan0{a} 
  libmpx0{a} libstdc++-5-dev{a} libtsan0{a} libubsan0{a} linux-libc-dev{a} 
  manpages-dev{a} 
0 個のパッケージを更新、 18 個を新たにインストール、 0 個を削除予定、0 個が更新されていない。
26.5 M バイトのアーカイブを取得する必要があります。 展開後に 101 M バイトのディスク領域が新たに消費されます。
以下のパッケージには満たされていない依存関係があります:
 libc6-dev : 依存: libc6 (= 2.23-0ubuntu3) [2.23-0ubuntu7 が既にインストール済みです]
以下のアクションでこれらの依存関係の問題は解決されます:

     以下のパッケージを現在のバージョンに一時固定する:    
1)     g++ [インストールされていません]                   
2)     g++-5 [インストールされていません]                 
3)     libc6-dev [インストールされていません]             
4)     libstdc++-5-dev [インストールされていません]       

     以下の依存関係を未解決のままにする:                  
5)     gcc が libc6-dev | libc-dev を推奨                 
6)     gcc-5 が libc6-dev (>= 2.13-0ubuntu6) を推奨       
7)     libgcc-5-dev が libc6-dev (>= 2.13-0ubuntu6) を推奨

Yで答えましたが、まだgccもg++も利用できません。再びsudo aptitude install g++で、いちどnしたあとの

以下のアクションでこれらの依存関係の問題は解決されます:

     以下のパッケージをダウングレードする:                      
1)     libc6 [2.23-0ubuntu7 (now) -> 2.23-0ubuntu3 (xenial)]    
2)     libc6-dbg [2.23-0ubuntu7 (now) -> 2.23-0ubuntu3 (xenial)]

Yで受け入れると、gccとg++が使えるようになりました。