Pure DataのPatchからNTS-1 mkIIのOSCユニットを作ってみた(2)サイズの削減

先日、Pure DataのパッチをhvccでCに変換してNTS-1 mkIIで動かしてみましたが、単なる鋸波のオシレータに29Kbも使っていました。

$ size ./sawosc.nts1mkiiunit 
   text	   data	    bss	    dec	    hex	filename
  28018	    964	     92	  29074	   7192	./sawosc.nts1mkiiunit

最初はそんなものかなと思っていたのですが、Cのコードの量から考えると、もっと小さくなっても良さそうです。削れるものがあれば、フットプリントの制限が32Kbであるディレイユニットやリバーブユニットでも、Pure Dataを使えるかもしれません。

まずは、ファイルサイズの詳しい内訳を見てみました。

$ size -A ./sawosc.nts1mkiiunit 
./sawosc.nts1mkiiunit  :
section            size    addr
.hash              2164     212
.dynsym            4416    2376
.dynstr            5766    6792
.rel.plt            712   13440
.text             11824   14152
.plt               1440   25976
.unit_header        816   27416
.preinit_array        0   28232
.init_array           0   28232
.dynamic            152   28232
.got                392   28384
.data               420   28776
.bss                 88   29196
.stack                4   29296
.comment             73       0
.ARM.attributes      50       0
.rel.dyn            880   12560
Total             29197

これを見ると、.dynsymや.dynstrなど、動的リンク用のデータが占める割合が結構大きい印象です。この中身を見てみます。

$ readelf -p .dynstr ./sawosc.nts1mkiiunit 

String dump of section '.dynstr':
  [     1]  unit_header
  [     d]  hLp_init
  [    16]  malloc
  [    1d]  hLp_free
  [    26]  hLp_hasData
  [    32]  hLp_getWriteBuffer
  [    45]  hLp_produce
  [    51]  hLp_getReadBuffer
  [    63]  hLp_consume
  [    6f]  hLp_reset
  [    79]  memset
  [    80]  mp_init
  [    88]  mp_free
  [    90]  mp_freeMessage
  [    9f]  mp_addMessage
  [    ad]  msg_copyToBuffer
  [    be]  sPhasor_init
  [    cb]  sPhasor_onMessage
  [    dd]  sPhasor_k_init
  [    ec]  sPhasor_k_onMessage
  [   100]  hv_getSampleRate
  [   111]  hv_string_to_hash
  [   123]  strlen
  [   12a]  msg_initWithFloat
  [   13c]  msg_initWithBang
  [   14d]  msg_initWithSymbol
  [   160]  msg_initWithHash
  [   171]  memcpy
  [   178]  strncpy
  [   180]  msg_compareSymbol
  [   192]  strcmp
  [   199]  msg_equalsElement
  [   1ab]  msg_setElementToFrom
  [   1c0]  snprintf
  [   1c9]  mq_initWithPoolSize
  [   1dd]  mq_size
  [   1e5]  mq_addMessage
  [   1f3]  mq_addMessageByTimestamp
  [   20c]  mq_pop
  [   213]  mq_removeMessage
  [   224]  mq_clear
  [   22d]  mq_free
  [   235]  mq_clearAfter
  [   243]  hTable_init
  [   24f]  hTable_initWithData
  [   263]  hTable_initWithFinalData
  [   27c]  hTable_free
  [   288]  hTable_resize
  [   296]  realloc
  [   29e]  hTable_onMessage
  [   2af]  ceilf
  [   2b5]  unit_init
  [   2bf]  unit_teardown
  [   2cd]  unit_reset
  [   2d8]  unit_resume
  [   2e4]  unit_suspend
  [   2f1]  unit_render
  [   2fd]  unit_get_param_value
  [   312]  unit_get_param_str_value
  [   32b]  unit_set_param_value
  [   340]  unit_set_tempo
  [   34f]  unit_tempo_4ppqn_tick
  [   365]  unit_note_on
  [   372]  unit_note_off
  [   380]  unit_all_note_off
  [   392]  unit_pitch_bend
  [   3a2]  unit_channel_pressure
  [   3b8]  unit_aftertouch
  [   3c8]  hv_heavy_new
  [   3d5]  hv_getNumOutputChannels
  [   3ed]  hv_sendFloatToReceiver
  [   404]  hv_processInline
  [   415]  midi_to_hz_lut_f
  [   426]  _ZN12HeavyContext7getSizeEv
  [   442]  _ZN12HeavyContext13getSampleRateEv
  [   465]  _ZN12HeavyContext16getCurrentSampleEv
  [   48b]  _ZN12HeavyContext21samplesToMillisecondsEm
  [   4b6]  _ZN12HeavyContext11setUserDataEPv
  [   4d8]  _ZN12HeavyContext11getUserDataEv
  [   4f9]  _ZN12HeavyContext11setSendHookEPFvP21HeavyContextInterfacePKcmPK9HvMessageE
  [   545]  _ZN12HeavyContext11getSendHookEv
  [   566]  _ZN12HeavyContext12setPrintHookEPFvP21HeavyContextInterfacePKcS3_PK9HvMessageE
  [   5b5]  _ZN12HeavyContext12getPrintHookEv
  [   5d7]  _ZN12HeavyContext17getBufferForTableEm
  [   5fe]  _ZN12HeavyContext17getLengthForTableEm
  [   625]  _Z15defaultSendHookP21HeavyContextInterfacePKcmPK9HvMessage
  [   661]  _ZN12HeavyContext11lockReleaseEv
  [   682]  _ZN12HeavyContext24setInputMessageQueueSizeEi
  [   6b0]  _ZN12HeavyContext25setOutputMessageQueueSizeEi
  [   6df]  _ZN12HeavyContext18sendBangToReceiverEm
  [   707]  _ZN12HeavyContext19sendFloatToReceiverEmf
  [   731]  _ZN12HeavyContext20sendSymbolToReceiverEmPKc
  [   75e]  _ZN12HeavyContext22sendMessageToReceiverVEmdPKcz
  [   78f]  fmax
  [   794]  _ZN12HeavyContext11lockAcquireEv
  [   7b5]  _ZN12HeavyContext7lockTryEv
  [   7d1]  _ZN12HeavyContext13cancelMessageEP9HvMessagePFvP21HeavyContextInterfaceiPKS0_E
  [   820]  _ZN12HeavyContext17setLengthForTableEmm
  [   848]  _ZN12HeavyContext18getNextSentMessageEPmP9HvMessagej
  [   87d]  _ZN12HeavyContext21millisecondsToSamplesEf
  [   8a8]  fmaxf
  [   8ae]  _ZN12HeavyContext21sendMessageToReceiverEmdP9HvMessage
  [   8e5]  _ZN12HeavyContextC2Ediii
  [   8fe]  _ZTV12HeavyContext
  [   911]  _ZN12HeavyContextC1Ediii
  [   92a]  _ZN12HeavyContextD2Ev
  [   940]  _ZN12HeavyContextD1Ev
  [   956]  _ZN12HeavyContextD0Ev
  [   96c]  _ZdlPv
  [   973]  _ZN12HeavyContext24scheduleMessageForObjectEPK9HvMessagePFvP21HeavyContextInterfaceiS2_Ei
  [   9cd]  _ZN12HeavyContext16getHashForStringEPKc
  [   9f5]  _Z13_hv_table_getP21HeavyContextInterfacem
  [   a20]  _Z30_hv_scheduleMessageForReceiverP21HeavyContextInterfacemP9HvMessage
  [   a67]  _Z28_hv_scheduleMessageForObjectP21HeavyContextInterfacePK9HvMessagePFvS0_iS3_Ei
  [   ab8]  hv_table_get
  [   ac5]  hv_scheduleMessageForReceiver
  [   ae3]  hv_scheduleMessageForObject
  [   aff]  __cxa_pure_virtual
  [   b12]  _ZN11Heavy_heavy7getNameEv
  [   b2d]  _ZN11Heavy_heavy19getNumInputChannelsEv
  [   b55]  _ZN11Heavy_heavy20getNumOutputChannelsEv
  [   b7e]  _ZN11Heavy_heavy15getTableForHashEm
  [   ba2]  _ZN11Heavy_heavy13processInlineEPfS0_i
  [   bc9]  _ZN11Heavy_heavy24processInlineInterleavedEPfS0_i
  [   bfb]  _ZN11Heavy_heavyD2Ev
  [   c10]  _ZTV11Heavy_heavy
  [   c22]  _ZN11Heavy_heavyD1Ev
  [   c37]  _ZN11Heavy_heavyD0Ev
  [   c4c]  _ZN11Heavy_heavy29cReceive_5CyYSrdm_sendMessageEP21HeavyContextInterfaceiPK9HvMessage
  [   ca2]  _ZN11Heavy_heavy7processEPPfS1_i
  [   cc3]  _ZN11Heavy_heavy16getParameterInfoEiP15HvParameterInfo
  [   cfa]  _ZN11Heavy_heavy26scheduleMessageForReceiverEmP9HvMessage
  [   d34]  hv_heavy_free
  [   d42]  _ZN11Heavy_heavyC2Ediii
  [   d5a]  _ZN11Heavy_heavyC1Ediii
  [   d72]  hv_heavy_new_with_options
  [   d8c]  hv_table_setLength
  [   d9f]  hv_table_getBuffer
  [   db2]  hv_table_getLength
  [   dc5]  hv_msg_getByteSize
  [   dd8]  hv_msg_init
  [   de4]  hv_msg_getNumElements
  [   dfa]  hv_msg_getTimestamp
  [   e0e]  hv_msg_setTimestamp
  [   e22]  hv_msg_isBang
  [   e30]  hv_msg_setBang
  [   e3f]  hv_msg_isFloat
  [   e4e]  hv_msg_getFloat
  [   e5e]  hv_msg_setFloat
  [   e6e]  hv_msg_isSymbol
  [   e7e]  hv_msg_getSymbol
  [   e8f]  hv_msg_setSymbol
  [   ea0]  hv_msg_isHash
  [   eae]  hv_msg_getHash
  [   ebd]  hv_msg_hasFormat
  [   ece]  hv_msg_toString
  [   ede]  hv_msg_copy
  [   eea]  hv_msg_free
  [   ef6]  hv_getSize
  [   f01]  hv_getNumInputChannels
  [   f18]  hv_setPrintHook
  [   f28]  hv_getPrintHook
  [   f38]  hv_setSendHook
  [   f47]  hv_stringToHash
  [   f57]  hv_sendBangToReceiver
  [   f6d]  hv_sendSymbolToReceiver
  [   f85]  hv_sendMessageToReceiverV
  [   f9f]  hv_sendMessageToReceiverFF
  [   fba]  hv_sendMessageToReceiverFFF
  [   fd6]  hv_sendMessageToReceiver
  [   fef]  hv_cancelMessage
  [  1000]  hv_getName
  [  100b]  hv_setUserData
  [  101a]  hv_getUserData
  [  1029]  hv_getCurrentTime
  [  103b]  hv_getCurrentSample
  [  104f]  hv_samplesToMilliseconds
  [  1068]  hv_millisecondsToSamples
  [  1081]  hv_getParameterInfo
  [  1095]  hv_lock_acquire
  [  10a5]  hv_lock_try
  [  10b1]  hv_lock_release
  [  10c1]  hv_setInputMessageQueueSize
  [  10dd]  hv_setOutputMessageQueueSize
  [  10fa]  hv_getNextSentMessage
  [  1110]  hv_process
  [  111b]  hv_processInlineInterleaved
  [  1137]  hv_delete
  [  1141]  _malloc_r
  [  114b]  _free_r
  [  1153]  __malloc_lock
  [  1161]  __malloc_unlock
  [  1171]  __malloc_free_list
  [  1184]  _sbrk_r
  [  118c]  __malloc_sbrk_start
  [  11a0]  _realloc_r
  [  11ab]  _sbrk
  [  11b1]  errno
  [  11b7]  _global_impure_ptr
  [  11ca]  __sf_fake_stdin
  [  11da]  __sf_fake_stdout
  [  11eb]  __sf_fake_stderr
  [  11fc]  __retarget_lock_acquire_recursive
  [  121e]  __lock___malloc_recursive_mutex
  [  123e]  __retarget_lock_release_recursive
  [  1260]  _malloc_usable_size_r
  [  1276]  cleanup_glue
  [  1283]  _reclaim_reent
  [  1292]  __retarget_lock_init
  [  12a7]  __retarget_lock_init_recursive
  [  12c6]  __retarget_lock_close
  [  12dc]  __retarget_lock_close_recursive
  [  12fc]  __retarget_lock_acquire
  [  1314]  __retarget_lock_try_acquire
  [  1330]  __retarget_lock_try_acquire_recursive
  [  1356]  __retarget_lock_release
  [  136e]  __lock___arc4random_mutex
  [  1388]  __lock___dd_hash_mutex
  [  139f]  __lock___tz_mutex
  [  13b1]  __lock___env_recursive_mutex
  [  13ce]  __lock___at_quick_exit_mutex
  [  13eb]  __lock___atexit_recursive_mutex
  [  140b]  __lock___sfp_recursive_mutex
  [  1428]  __lock___sinit_recursive_mutex
  [  1447]  __fpclassifyd
  [  1455]  __fpclassifyf
  [  1463]  _ZSt9terminatev
  [  1473]  __cxa_deleted_virtual
  [  1489]  _ZN10__cxxabiv111__terminateEPFvvE
  [  14ac]  abort
  [  14b2]  _ZSt13set_terminatePFvvE
  [  14cb]  _ZN10__cxxabiv119__terminate_handlerE
  [  14f1]  _ZSt13get_terminatev
  [  1506]  _ZN10__cxxabiv112__unexpectedEPFvvE
  [  152a]  _ZSt14set_unexpectedPFvvE
  [  1544]  _ZN10__cxxabiv120__unexpected_handlerE
  [  156b]  _ZSt14get_unexpectedv
  [  1581]  _ZSt10unexpectedv
  [  1593]  raise
  [  1599]  _exit
  [  159f]  _init_signal_r
  [  15ae]  _raise_r
  [  15b7]  _getpid_r
  [  15c1]  _kill_r
  [  15c9]  __sigtramp_r
  [  15d6]  _init_signal
  [  15e3]  __sigtramp
  [  15ee]  _kill
  [  15f4]  _getpid
  [  15fc]  __heap_size
  [  1608]  __stack_size
  [  1615]  __data_start__
  [  1624]  __data_end__
  [  1631]  __bss_start__
  [  163f]  __bss_end__
  [  164b]  __HEAP
  [  1652]  __HEAP_END
  [  165d]  _stack_end
  [  1668]  _stack_addr
  [  1674]  __stack
  [  167c]  __SP_INIT

この結果を見ると、hvccが生成したコードで使用されているシンボルが沢山含まれています。

実際には、hvccが生成した関数や変数はユニットの外部から直接アクセスされないので、テーブルに載せておく必要はありません。ユニットの外部からアクセスされる関数・変数は、unit_で始まるものだけのはずです。

gccには、-fvisibility=hiddenというオプションがあり、これを使えばextern宣言していないシンボルはテーブルに載らなくなります。これをconfig.mkUDEFSに追加して試してみます。

$ size ./sawosc.nts1mkiiunit 
   text	   data	    bss	    dec	    hex	filename
  18151	    760	     92	  19003	   4a3b	./sawosc.nts1mkiiunit

サイズが大幅に減りました。ただ、これをNTS-1 mkIIに転送するためにKONTROL Editorにドラッグ&ドロップすると、KONGROL Editorが落ちてしまいます。改めてシンボルテーブルを調べてみると、外部から呼び出される、unit_で始まるシンボルまで消えてしまっていました。

対処としては、テーブルに残したい関数や変数にはvisibility("default")というアトリビュートを付けます。このアトリビュートが付いたシンボルは-fvisibility=hiddenの効果を逃れます。

logue SDKの構造上、このアトリビュートはattributes.hの中で#defineされているマクロに、以下のような感じで追加するのが良さそうです。

common/attributes.h:

#define __unit_callback __attribute__((used, visibility("default")))
#define __unit_header __attribute__((used, section(".unit_header") ,visibility("default")))

ここからだんだん話がややこしくなってくるのですが、いろいろ実験してみた限りでは、このアトリビュートはソースファイルでの実装だけでなく、ヘッダファイルでの宣言にもつける必要があるようです。

たとえば、unit_で始まる関数は、ユニットのプロジェクトごとのunit.ccの中で実装されています。

一方、関数の宣言はSDKのフォルダ(logue-sdk/platforms/nts-1_mkii/common)内のヘッダファイルで行われています。具体的には、common/unit.hが関数の宣言、common/_unit_base.c__attribute__((weak)) を使ったデフォルト実装となっています。

ところが、common/unit.hでは、各関数を__unit_header無しで宣言しています。そのため、attributes.hで追加したvisibility("default")は適用されません。関数を実装するunit.ccの中で、これらの関数をvisibility("default")付きで実装しても、アトリビュートなしのヘッダファイルの宣言のほうが優先されてしまいます。従って、visibility("default")をunit_で始まる関数群に適用するためには、common/unit.hの中でのこれらの関数の宣言に__unit_headerを追加することが必要になります。

まとめると、visibility("default")を有効にするには、

・attributes.h の__unit_headerマクロにvisibility("default")を追加する
・unit.h の各関数の宣言に__unit_headerを追加する

ことが必要です。

SDKの複数のファイルに変更が必要になるので、common/配下で共有されているファイルそのものを変更するか、個別のユニットのディレクトリ内のファイルの変更で対応するか、悩ましいところです。今回はひとまずユニット内のファイルの変更だけで対応してみます。

それには、

・unit.hとattributes.hをプロジェクトのディレクトリにコピーして変更
・unit.hを必要とするソースコード(少なくとも、unit.cc、osc.h、header.c、それに_unit_base.c)が変更後のunit.hを読むようにする

必要があります。

以下、実際の作業です。

まず、変更しなければならないSDKのファイルをプロジェクトのディレクトリにコピーしてきます。目印としてファイル名の最後に”-v”を付けておきます。

$ cp ../common/attributes.h ./attributes-v.h
$ cp ../common-ex/unit.h ./unit-v.h
$ cp ../common/_unit_base.c ./_unit_base-v.c

そして、これらを書き換えます。以下は書き換えた結果のファイルです。

attributes-v.h

unit-v.h

また、header.c、unit.cc、osc.hは、以下のようにunit-v.hをunit_osc.hよりも先にincludeするように書き換えます。unit_osc.hは(オリジナルの)unit.hをincludeしていますが、先にunit-v.hが読み込まれていれば#ifndef UNIT_H_が成立しないのでunit.hの中身は読み込まれません。

#include "unit-v.h"
#include "unit_osc.h" 

_unit_base-v.cは、unit.hの代わりにunit-v.hをincludeするように書き換えます。(実際には、_unit_base.cで定義されている関数はすべてunit.ccにある定義で置き換えられるはずですが、unit.hが残っているのは気持ち悪いので。)(
さらに、Makefileの中で、以下のように、_unit_base.cの代わりに_unit_base-v.cを使うように書き換えます。

CSRC := $(UCSRC)
#CSRC += $(realpath $(COMMON_SRC_PATH)/_unit_base.c)                            
CSRC += $(realpath $(COMMON_SRC_PATH)/_unit_base-v.c)

これで、ようやく準備が整いました。

ビルドしてみると10KB弱の領域が解放されました。今回は元のPure Dataパッチがシンプルでしたが、より複雑なパッチならさらに効果は大きくなるでしょう。

$ size -A ./sawosc.nts1mkiiunit 
./sawosc.nts1mkiiunit  :
section            size    addr
.hash               852     212
.dynsym            1824    1064
.dynstr            1762    2888
.rel.plt            336    5500
.text             11748    5840
.plt                688   17588
.unit_header        408   18280
.preinit_array        0   18688
.init_array           0   18688
.dynamic            152   18688
.got                188   18840
.data               420   19028
.bss                 88   19448
.stack                4   19536
.comment             73       0
.ARM.attributes      50       0
.rel.dyn            848    4652
Total             19441

下のグラフはbefore-afterの比較です。セクションのサイズの順にソートしています。最大を占めるのはもちろんコードですが、それに次ぐサイズだった動的リンク関連のデータが大幅に減っているのが分かると思います。

今回のシンプルなPure Dataパッチでは、Cに変換してビルドした結果のバイナリサイズが16KBを超えているので、モジュレーションユニットを作ることは難しそうです。ディレイやリバーブでは、SDRAM領域をどうやってPure Dataパッチから使うかという別の課題がありますが、それはまた別途検討したいと思います。NTS-3では、ユニットのフットプリント制約は24Kbなので、簡易的なものなら作れるかもしれません。

コメント